摄像机
透视摄像机PerspectiveCamera
import * as THREE from "../../three";
import { OrbitControls } from "../../three/examples/jsm/controls/OrbitControls";
import { GUI } from "dat.gui";
const canvas = document.querySelector("#canvas");
const view1Elem = document.querySelector("#view1");
const view2Elem = document.querySelector("#view2");
const renderer = new THREE.WebGLRenderer({ canvas });
const camera = new THREE.PerspectiveCamera(45, 2, 5, 100);
camera.position.set(0, 10, 20);
const scene = new THREE.Scene();
scene.background = new THREE.Color("black");
const cameraHelper = new THREE.CameraHelper(camera);
scene.add(cameraHelper);
class MinMaxGUIHelper {
constructor(obj, minProp, maxProp, minDif) {
this.obj = obj;
this.minProp = minProp;
this.maxProp = maxProp;
this.minDif = minDif;
}
get min() {
return this.obj[this.minProp];
}
set min(v) {
this.obj[this.minProp] = v;
this.obj[this.maxProp] = Math.max(this.obj[this.maxProp], v + this.minDif);
}
get max() {
return this.obj[this.maxProp];
}
set max(v) {
this.obj[this.maxProp] = v;
this.min = this.min; // this will call the min setter
}
}
const gui = new GUI();
gui.add(camera, "fov", 1, 180);
const minMaxGUIHelper = new MinMaxGUIHelper(camera, "near", "far", 0.1);
gui.add(minMaxGUIHelper, "min", 0.1, 50, 0.1).name("near");
gui.add(minMaxGUIHelper, "max", 0.1, 50, 0.1).name("far");
const controls = new OrbitControls(camera, view1Elem);
controls.target.set(0, 5, 0);
controls.update();
const camera2 = new THREE.PerspectiveCamera(60, 2, 0.1, 500);
camera2.position.set(40, 10, 40);
camera2.lookAt(0, 5, 0);
const controls2 = new OrbitControls(camera2, view2Elem);
controls2.target.set(0, 5, 0);
controls2.update();
{
const planeSize = 40;
const loader = new THREE.TextureLoader();
const texture = loader.load("./assets/images/checker.png");
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
const repeats = planeSize / 2;
texture.repeat.set(repeats, repeats);
const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshPhongMaterial({
map: texture,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.rotation.x = Math.PI * -0.5;
scene.add(mesh);
}
{
const cubeSize = 4;
const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
const cubeMat = new THREE.MeshPhongMaterial({ color: "#8AC" });
const mesh = new THREE.Mesh(cubeGeo, cubeMat);
mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
scene.add(mesh);
}
{
const sphereRadius = 3;
const sphereWidthDivisions = 32;
const sphereHeightDivisions = 16;
const sphereGeo = new THREE.SphereGeometry(
sphereRadius,
sphereWidthDivisions,
sphereHeightDivisions
);
const sphereMat = new THREE.MeshPhongMaterial({ color: "#CA8" });
const mesh = new THREE.Mesh(sphereGeo, sphereMat);
mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
scene.add(mesh);
}
{
const color = 0xffffff;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(0, 10, 0);
light.target.position.set(-5, 0, 0);
scene.add(light);
scene.add(light.target);
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function setScissorForElement(elem) {
const canvasRect = canvas.getBoundingClientRect();
const elemRect = elem.getBoundingClientRect();
const right = Math.min(elemRect.right, canvasRect.right) - canvasRect.left;
const left = Math.max(0, elemRect.left - canvasRect.left);
const bottom = Math.min(elemRect.bottom, canvasRect.bottom) - canvasRect.top;
const top = Math.max(0, elemRect.top - canvasRect.top);
const width = Math.min(canvasRect.width, right - left);
const height = Math.min(canvasRect.height, bottom - top);
const positiveYUpBottom = canvasRect.height - bottom;
renderer.setScissor(left, positiveYUpBottom, width, height);
renderer.setViewport(left, positiveYUpBottom, width, height);
return width / height;
}
function render() {
resizeRendererToDisplaySize(renderer);
renderer.setScissorTest(true);
{
const aspect = setScissorForElement(view1Elem);
camera.aspect = aspect;
camera.updateProjectionMatrix();
cameraHelper.update();
cameraHelper.visible = false;
scene.background.set(0x000000);
renderer.render(scene, camera);
}
{
const aspect = setScissorForElement(view2Elem);
camera2.aspect = aspect;
camera2.updateProjectionMatrix();
cameraHelper.visible = true;
scene.background.set(0x000040);
renderer.render(scene, camera2);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
z冲突问题
解决方法1可以在创建WebGLRenderer时开启logarithmicDepthBuffer为true但会大大降低运行速度。
解决方法2好好抉择near和far的设置
正交摄像机OrthographicCamera
import * as THREE from "../../three/build/three";
import { OrbitControls } from "../../three/examples/jsm/controls/OrbitControls";
import { GUI } from "dat.gui";
const canvas = document.querySelector("#canvas");
const view1Elem = document.querySelector("#view1");
const view2Elem = document.querySelector("#view2");
const renderer = new THREE.WebGLRenderer({ canvas });
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 5, 50);
camera.zoom = 0.2;
camera.position.set(0, 10, 20);
const scene = new THREE.Scene();
scene.background = new THREE.Color("black");
const cameraHelper = new THREE.CameraHelper(camera);
scene.add(cameraHelper);
class MinMaxGUIHelper {
constructor(obj, minProp, maxProp, minDif) {
this.obj = obj;
this.minProp = minProp;
this.maxProp = maxProp;
this.minDif = minDif;
}
get min() {
return this.obj[this.minProp];
}
set min(v) {
this.obj[this.minProp] = v;
this.obj[this.maxProp] = Math.max(this.obj[this.maxProp], v + this.minDif);
}
get max() {
return this.obj[this.maxProp];
}
set max(v) {
this.obj[this.maxProp] = v;
this.min = this.min; // this will call the min setter
}
}
const gui = new GUI();
gui.add(camera, "zoom", 0.01, 1, 0.01).listen();
const minMaxGUIHelper = new MinMaxGUIHelper(camera, "near", "far", 0.1);
gui.add(minMaxGUIHelper, "min", 0.1, 50, 0.1).name("near");
gui.add(minMaxGUIHelper, "max", 0.1, 50, 0.1).name("far");
const controls = new OrbitControls(camera, view1Elem);
controls.target.set(0, 5, 0);
controls.update();
const camera2 = new THREE.PerspectiveCamera(60, 2, 0.1, 500);
camera2.position.set(16, 28, 40);
camera2.lookAt(0, 5, 0);
const controls2 = new OrbitControls(camera2, view2Elem);
controls2.target.set(0, 5, 0);
controls2.update();
{
const planeSize = 40;
const loader = new THREE.TextureLoader();
const texture = loader.load("./assets/images/checker.png");
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
const repeats = planeSize / 2;
texture.repeat.set(repeats, repeats);
const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshPhongMaterial({
map: texture,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.rotation.x = Math.PI * -0.5;
scene.add(mesh);
}
{
const cubeSize = 4;
const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
const cubeMat = new THREE.MeshPhongMaterial({ color: "#8AC" });
const mesh = new THREE.Mesh(cubeGeo, cubeMat);
mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
scene.add(mesh);
}
{
const sphereRadius = 3;
const sphereWidthDivisions = 32;
const sphereHeightDivisions = 16;
const sphereGeo = new THREE.SphereGeometry(
sphereRadius,
sphereWidthDivisions,
sphereHeightDivisions
);
const sphereMat = new THREE.MeshPhongMaterial({ color: "#CA8" });
const mesh = new THREE.Mesh(sphereGeo, sphereMat);
mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
scene.add(mesh);
}
{
const color = 0xffffff;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(0, 10, 0);
light.target.position.set(-5, 0, 0);
scene.add(light);
scene.add(light.target);
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function setScissorForElement(elem) {
const canvasRect = canvas.getBoundingClientRect();
const elemRect = elem.getBoundingClientRect();
const right = Math.min(elemRect.right, canvasRect.right) - canvasRect.left;
const left = Math.max(0, elemRect.left - canvasRect.left);
const bottom = Math.min(elemRect.bottom, canvasRect.bottom) - canvasRect.top;
const top = Math.max(0, elemRect.top - canvasRect.top);
const width = Math.min(canvasRect.width, right - left);
const height = Math.min(canvasRect.height, bottom - top);
const positiveYUpBottom = canvasRect.height - bottom;
renderer.setScissor(left, positiveYUpBottom, width, height);
renderer.setViewport(left, positiveYUpBottom, width, height);
return width / height;
}
function render() {
resizeRendererToDisplaySize(renderer);
renderer.setScissorTest(true);
{
const aspect = setScissorForElement(view1Elem);
camera.left = -aspect;
camera.right = aspect;
camera.updateProjectionMatrix();
cameraHelper.update();
cameraHelper.visible = false;
scene.background.set(0x000000);
renderer.render(scene, camera);
}
{
const aspect = setScissorForElement(view2Elem);
camera2.aspect = aspect;
camera2.updateProjectionMatrix();
cameraHelper.visible = true;
scene.background.set(0x000040);
renderer.render(scene, camera2);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
阴影
Three.js 默认使用shadow maps(阴影贴图),阴影贴图的工作方式就是具有投射阴影的光能对所有能被投射阴影的物体从光源渲染阴影。
当多个灯光都可以投射阴影时通常会存在性能问题。常用的解决方式有
- 允许多个光源,但只让一个光源能投射阴影。
- 使用光照贴图或者环境光贴图,预先计算离线照明的效果。
- 使用假阴影
import * as THREE from "../../three";
const canvas = document.querySelector("#canvas");
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.physicallyCorrectLights = true;
const camera = new THREE.PerspectiveCamera(45, 2, 0.1, 100);
camera.position.set(0, 10, 20);
camera.lookAt(0, 0, 0);
const scene = new THREE.Scene();
scene.background = new THREE.Color("white");
const loader = new THREE.TextureLoader();
{
const planeSize = 40;
const texture = loader.load("./assets/images/checker.png");
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
const repeats = planeSize / 2;
texture.repeat.set(repeats, repeats);
const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide,
});
planeMat.color.setRGB(1.5, 1.5, 1.5);
const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.rotation.x = Math.PI * -0.5;
scene.add(mesh);
}
const shadowTexture = loader.load("./assets/images/roundshadow.png");
const sphereShadowBases = [];
{
const sphereRadius = 1;
const sphereGeo = new THREE.SphereGeometry(sphereRadius, 32, 16);
const planeSize = 1;
const shadowGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const numSpheres = 15;
for (let i = 0; i < numSpheres; ++i) {
const base = new THREE.Object3D();
scene.add(base);
const u = i / numSpheres;
const sphereMat = new THREE.MeshPhongMaterial();
sphereMat.color.setHSL(u, 1, 0.75);
const sphereMesh = new THREE.Mesh(sphereGeo, sphereMat);
sphereMesh.position.set(0, sphereRadius + 2, 0);
base.add(sphereMesh);
const shadowMat = new THREE.MeshBasicMaterial({
map: shadowTexture,
transparent: true,
depthWrite: false,
});
const shadowMesh = new THREE.Mesh(shadowGeo, shadowMat);
shadowMesh.position.y = 0.001;
shadowMesh.rotation.x = Math.PI * -0.5;
const shadowSize = sphereRadius * 4;
shadowMesh.scale.set(shadowSize, shadowSize, shadowSize);
base.add(shadowMesh);
sphereShadowBases.push({
base,
sphereMesh,
shadowMesh,
y: sphereMesh.position.y,
});
}
}
{
const skyColor = 0xb1e1ff;
const groundColor = 0xb97a20;
const intensity = 2;
const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
scene.add(light);
}
{
const color = 0xffffff;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(0, 10, 5);
light.target.position.set(-5, 0, 0);
scene.add(light);
scene.add(light.target);
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
sphereShadowBases.forEach((sphereShadowBase, ndx) => {
const { base, sphereMesh, shadowMesh, y } = sphereShadowBase;
const u = ndx / sphereShadowBases.length;
const speed = time * 0.2;
const angle = speed + u * Math.PI * 2 * (ndx % 1 ? 1 : -1);
const radius = Math.sin(speed - ndx) * 10;
base.position.set(Math.cos(angle) * radius, 0, Math.sin(angle) * radius);
const yOff = Math.abs(Math.sin(time * 2 + ndx));
sphereMesh.position.y = y + THREE.MathUtils.lerp(-2, 2, yOff);
shadowMesh.material.opacity = THREE.MathUtils.lerp(1, 0.25, yOff);
});
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
上面的例子是使用加贴图的实现。
实际可以投射阴影的光有DirectionalLight,PointLight,SpotLight
//开启阴影属性
renderer.shadowMap.enabled = true
// 光能投射阴影
light.castShadow=true
//mesh投射接收阴影
mesh.castShadow = true
mesh.receiveShadow = true
光源的阴影相机决定了阴影投射的区域。
通过调整光源的阴影相机来调整该盒子的大小。
为什么不把阴影投射区域设为很大?
被投射产生的阴影有纹理有单位大小。投射的区域很大意味着投射的阴影会块状化。可以设置light.shadow.mapSize.width
和 light.shadow.mapSize.height
来设置阴影的纹理分辨率。默认为 512X512。如果设置的很大,他们在计算时将占用更多的内存,并且变得很慢。为了获得更真实的阴影,应该尽量将值设置的最小。renderer.capabilities.maxTextureSize
对于每个用户都有最大纹理的上限值。
雾
通过创建 Fog
||FogExp2
实例并设定scene的fog
属性。
Fog
让你设定 near
和 far
属性,代表距离摄像机的距离。任何物体比 near
近不会受到影响,任何物体比 far
远则完全是雾的颜色。在 near
和 far
中间的物体,会从它们自身材料的颜色褪色到雾的颜色。
FogExp2
会根据离摄像机的距离呈指数增长。
渲染目标
即threejs对帧缓存的实现
import * as THREE from "../../three";
const canvas = document.querySelector("#canvas");
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, 2, 0.1, 5);
camera.position.z = 2;
{
const color = 0xffffff;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
const renderTarget = new THREE.WebGLRenderTarget(512, 512);
const rtCamera = new THREE.PerspectiveCamera(75, 1, 0.1, 5);
rtCamera.position.z = 2;
const rtScene = new THREE.Scene();
rtScene.background = new THREE.Color("red");
{
const color = 0xffffff;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
rtScene.add(light);
}
const geometry = new THREE.BoxGeometry(1, 1, 1);
function makeInstance(geometry, color, x) {
const material = new THREE.MeshPhongMaterial({ color });
const cube = new THREE.Mesh(geometry, material);
rtScene.add(cube);
cube.position.x = x;
return cube;
}
const rtCubes = [
makeInstance(geometry, 0x44aa88, 0),
makeInstance(geometry, 0x8844aa, -2),
makeInstance(geometry, 0xaa8844, 2),
];
const material = new THREE.MeshPhongMaterial({
map: renderTarget.texture,
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
rtCubes.forEach((cube, ndx) => {
const speed = 1 + ndx * 0.1;
const rot = time * speed;
cube.rotation.x = rot;
cube.rotation.y = rot;
});
renderer.setRenderTarget(renderTarget); //即绑定帧缓存
renderer.render(rtScene, rtCamera); //绘制帧缓存
renderer.setRenderTarget(null); //回到默认缓冲
cube.rotation.x = time;
cube.rotation.y = time * 1.1;
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
自定义缓冲几何体
import * as THREE from "../../three";
const canvas = document.querySelector("#canvas");
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, 2, 0.1, 100);
camera.position.z = 5;
{
const color = 0xffffff;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
const vertices = [
// front
{ pos: [-1, -1, 1], norm: [0, 0, 1], uv: [0, 0] }, // 0
{ pos: [1, -1, 1], norm: [0, 0, 1], uv: [1, 0] }, // 1
{ pos: [-1, 1, 1], norm: [0, 0, 1], uv: [0, 1] }, // 2
{ pos: [1, 1, 1], norm: [0, 0, 1], uv: [1, 1] }, // 3
// right
{ pos: [1, -1, 1], norm: [1, 0, 0], uv: [0, 0] }, // 4
{ pos: [1, -1, -1], norm: [1, 0, 0], uv: [1, 0] }, // 5
{ pos: [1, 1, 1], norm: [1, 0, 0], uv: [0, 1] }, // 6
{ pos: [1, 1, -1], norm: [1, 0, 0], uv: [1, 1] }, // 7
// back
{ pos: [1, -1, -1], norm: [0, 0, -1], uv: [0, 0] }, // 8
{ pos: [-1, -1, -1], norm: [0, 0, -1], uv: [1, 0] }, // 9
{ pos: [1, 1, -1], norm: [0, 0, -1], uv: [0, 1] }, // 10
{ pos: [-1, 1, -1], norm: [0, 0, -1], uv: [1, 1] }, // 11
// left
{ pos: [-1, -1, -1], norm: [-1, 0, 0], uv: [0, 0] }, // 12
{ pos: [-1, -1, 1], norm: [-1, 0, 0], uv: [1, 0] }, // 13
{ pos: [-1, 1, -1], norm: [-1, 0, 0], uv: [0, 1] }, // 14
{ pos: [-1, 1, 1], norm: [-1, 0, 0], uv: [1, 1] }, // 15
// top
{ pos: [1, 1, -1], norm: [0, 1, 0], uv: [0, 0] }, // 16
{ pos: [-1, 1, -1], norm: [0, 1, 0], uv: [1, 0] }, // 17
{ pos: [1, 1, 1], norm: [0, 1, 0], uv: [0, 1] }, // 18
{ pos: [-1, 1, 1], norm: [0, 1, 0], uv: [1, 1] }, // 19
// bottom
{ pos: [1, -1, 1], norm: [0, -1, 0], uv: [0, 0] }, // 20
{ pos: [-1, -1, 1], norm: [0, -1, 0], uv: [1, 0] }, // 21
{ pos: [1, -1, -1], norm: [0, -1, 0], uv: [0, 1] }, // 22
{ pos: [-1, -1, -1], norm: [0, -1, 0], uv: [1, 1] }, // 23
];
const positions = [];
const normals = [];
const uvs = [];
for (const vertex of vertices) {
positions.push(...vertex.pos);
normals.push(...vertex.norm);
uvs.push(...vertex.uv);
}
const geometry = new THREE.BufferGeometry();
const positionNumComponents = 3;
const normalNumComponents = 3;
const uvNumComponents = 2;
geometry.setAttribute(
"position",
new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents)
);
geometry.setAttribute(
"normal",
new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents)
);
geometry.setAttribute(
"uv",
new THREE.BufferAttribute(new Float32Array(uvs), uvNumComponents)
);
geometry.setIndex([
0,
1,
2,
2,
1,
3,
4,
5,
6,
6,
5,
7,
8,
9,
10,
10,
9,
11,
12,
13,
14,
14,
13,
15,
16,
17,
18,
18,
17,
19,
20,
21,
22,
22,
21,
23,
]);
const loader = new THREE.TextureLoader();
const texture = loader.load("./assets/images/star.png");
function makeInstance(geometry, color, x) {
const material = new THREE.MeshPhongMaterial({ color, map: texture });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
cube.position.x = x;
return cube;
}
const cubes = [
makeInstance(geometry, 0x88ff88, 0),
makeInstance(geometry, 0x8888ff, -4),
makeInstance(geometry, 0xff8888, 4),
];
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
cubes.forEach((cube, ndx) => {
const speed = 1 + ndx * 0.1;
const rot = time * speed;
cube.rotation.x = rot;
cube.rotation.y = rot;
});
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);