粒子
主要通过threejs
里的Points
和PointsMaterial
实现
Material
里的sizeAttenuation
属性,用于配置粒子的近大远小的效果(离摄像机越远则越小)
const particlesGeometry = new THREE.SphereGeometry(1, 32, 32);
const particlesMaterial = new THREE.PointsMaterial({
size: 0.03,
// sizeAttenuation: true, // 默认为true
});
const particles = new THREE.Points(particlesGeometry, particlesMaterial);
scene.add(particles);
此时,摄像机放大时,粒子看着也会大
假如sizeAttenuation
设为false
,则无论怎么放大,粒子都是同一尺寸:
分散粒子
如果要实现分散式的粒子,可以自己创建一个BufferGeometry
,通过循环简单的实现为:
const particlesMaterial = new THREE.PointsMaterial({
size: 0.1,
});
const count = 5000000;
const positions = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
positions[i] = (Math.random() - 0.5) * 5;
}
particlesGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3)
);
scene.add(particles);
粒子材质使用纹理
一个找粒子材质的免费分享网站:https://kenney.nl/assets/particle-pack
同样的,粒子材质也可以设置纹理,通过map
或alphaMap
等属性加载,不过通常会选择使用alphaMap
进行加载,需要提供一个黑色背景,内容白色的图片:
比如下面的例子中,使用这张图片:
声明材质时,使用alphaMap
和transparent
const particlesMaterial = new THREE.PointsMaterial({
size: 0.02,
transparent: true,
color: "#ff0",
alphaMap: particleTexture,
});
但是这样简单地设置还不够,可能会出现一些问题,比如下面这张图里,有的粒子图片的边缘还存在,会遮挡后面的粒子,有的粒子是正常的
原因:GPU在创建粒子的时候是按照顺序进行创建的,但webGL
渲染器并不知道哪一个在前哪一个在后,即不知道他们的深度关系是怎么样的
修复方式:
alphaTest
alphaTest
可以设置0-1
之间的值,用于告诉渲染器使用的纹理中,alpha
值低于该值的像素不进行渲染,alpha
值从0 - 1
代表从黑到白的渐变:
它的默认值是0,默认情况下表示只有像素alpha
值<=0
(纯黑色)的才会被忽略
所以可以通过设置合适的比例来让其渲染出来,比如设置0.5,那么图中alpha
值小于0.5
的颜色都不会被渲染出来
const particlesMaterial = new THREE.PointsMaterial({
size: 0.02,
transparent: true,
color: "#ff0",
alphaMap: particleTexture,
alphaTest: 0.5,
});
这样,就可以大概去除掉黑色的边框了:
注意,不要把这个和
opcaity
透明度混为一谈了
depthTest
depthTest
默认为true
,当它启动时,意味着webGL
渲染器会在渲染每个像素(物体就是若干个像素绘制出来的)时,计算已经绘制出来的像素与当前要绘制的像素的深度关系
如果当前要绘制像素比已经存在于深度缓冲区中的像素更远(或更近,取决于深度函数),那么这个像素将不会被绘制。这样可以确保只有“可见”的部分才会被绘制到屏幕上,从而实现正确的遮挡效果,同时节约渲染开销
将其设为false
,可以看到所有粒子的黑色边框都消除了
const particlesMaterial = new THREE.PointsMaterial({
size: 0.02,
transparent: true,
color: "#ff0",
alphaMap: particleTexture,
// alphaTest: 0.5,
depthTest: false
});
相比于alphaTest
,它不需要试验出合适的值,但alphaTest
相对会更定制化
一些
但是,如果场景中存在其他的物体,比如添加一个立方体,此时如果关闭了depthTest
,会出现下面的结果:
立方体后面的粒子也渲染了出来,预期应该是立方体遮挡其后方的粒子:
当然,除非你就想要类似"透视"的效果,你可以这么做
depthWrite
所有被webGL
渲染出来的东西,它们的depth
值都会被存放在内存的一个depth缓冲区
里
所以,当depthWrite
设置为true
时,物体将会更新深度缓冲区,使得后来渲染的对象可以根据新的深度值进行深度测试。如果设置为false
,则物体不会更新深度缓冲区,这意味着之后渲染的对象可能会忽略这个物体的存在,并可能被错误地渲染在其之上。
在当前的场景中,我们需要的就是立方体后面的粒子被隐藏
,所以可以使用depthWrite
const particlesMaterial = new THREE.PointsMaterial({
size: 0.02,
transparent: true,
color: "#ff0",
alphaMap: particleTexture,
depthWrite: false,
});
上述alphaTest
、depthTest
、depthWrite
都属于解决方案,没有完美的解决方案,但是通常对于粒子的场景都会使用DepthWrite: false
,也可以三者结合使用弄出更好的效果
Blending
(颜色)混合模式,即设置场景中物体重合时的展现形式,可以参考threejs
官方提供例子来理解,PS、PR等软件里也是有这种配置的,它们是同一个东西
在粒子的场景中,常用的是THREE.NormalBlending
和THREE.AdditiveBlending
,默认是THREE.NormalBlending
不同色的粒子
可以通过配置顶点颜色来实现不同色的粒子:
const count = 20000;
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
for (let i = 0; i < count * 3; i++) {
positions[i] = (Math.random() - 0.5) * 10;
colors[i] = Math.random();
}
particlesGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3)
);
particlesGeometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
颜色可以用RGB
控制,所以用一个colors
数组用来存放RGB颜色值,每3个代表一个顶点的颜色,然后给几何体设置上顶点颜色的attributes
即可
注意这样子配置后,几何体还没有顶点颜色的效果:
需要在粒子的材质里加上vertexColors: true
:
const particlesMaterial = new THREE.PointsMaterial({
size: 0.1, // 更大的粒子尺寸
transparent: true,
// color: "#ff0", // 注意要注释掉原本的粒子颜色,否则会默认把该颜色叠加在每个顶点的颜色上
alphaMap: particleTexture,
depthWrite: false,
vertexColors: true,
});
可以看到,粒子的颜色正确地使用上了,现在粒子重叠时是遮挡+覆盖的效果,可以配置blending: THREE.AdditiveBlending
来开启颜色叠加混合模式:
const particlesMaterial = new THREE.PointsMaterial({
size: 0.1,
transparent: true,
// color: "#ff0",
alphaMap: particleTexture,
depthWrite: false,
blending: THREE.AdditiveBlending, // 颜色叠加模式
vertexColors: true,
});
这样,下面框选的部分就会自动进行颜色叠加,计算出叠加后的颜色并渲染
粒子动画
粒子使用Points
创建,和Mesh
一样继承自Object3D
,所以也可以通过在每一帧中修改rotation
、position
等属性来实现动画,比如实现一个粒子的旋转:
const clock = new THREE.Clock();
const tick = () => {
const elapsedTime = clock.getElapsedTime();
// update Particles
particles.rotation.y = elapsedTime * 0.02;
particles.rotation.x = elapsedTime * 0.02;
controls.update();
// Render
renderer.render(scene, camera);
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
tick();
除此之外,也可以通过修改BufferGeometry
的坐标来实现动画:
for (let i = 0; i < count; i++) {
const i3 = i * 3;
particles.geometry.attributes.position.array[i3 + 1] =
Math.sin(elapsedTime);
}
创建粒子的BufferGeometry
时是经过count
次遍历计算出几何体的每一个顶点后生成的,通过下面的代码创建
particlesGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3)
);
所以particles.geometry.attributes.position.array[i3 + 1]
就是顶点的Y轴坐标,设置完成后得到下面的效果:
它们没有动起来,这是因为直接修改geometry
的几何坐标后还需要通知webGL
对其进行更新,需要加上下面的代码:
particles.geometry.attributes.position.needsUpdate = true;
效果:
当前的代码是在每一帧给所有粒子的y轴都设置为Math.sin(elapsedTime)
,所以效果是相同幅度的上下浮动,给他们加上各自的x
轴位置就可以实现波浪的效果了:
for (let i = 0; i < count; i++) {
const i3 = i * 3;
const x = particles.geometry.attributes.position.array[i3];
particles.geometry.attributes.position.array[i3 + 1] = Math.sin(
elapsedTime + x
);
}
但是显然,操作几何体的每一个坐标是很耗性能的,因为要在每一帧之内进行count
次的遍历,所以尽量不要这么去实现动画,而是用“着色器”实现一些更好的动画
生成银河系
银河系是一个螺旋状的形状,要生成银河系,首先要思考怎么生成螺旋的形状;而螺旋的形状实际上就是将多条直线从同一个起点出发并弯曲,以不同角度绕出的一个形状,下面这是最终可以生成的银河系效果:
生成直线
所以,我们需要先构造出4条直线,假设使用20000个粒子,那么应该每4个粒子是一轮计算,每轮计算中,每个粒子构成的角度应该是360° / 4 = 90°
,但是由于在threejs
里不使用角度作为度量单位,所以应该是2Π / 4 = Π / 2
;
for (let i = 0; i < params.count; i++) {
const i3 = i * 3;
// 假设params.branches = 4
const branchAngle = ((i % params.branches) / params.branches) * Math.PI * 2;
positions[i3] = branchAngle; // x轴坐标
positions[i3 + 1] = 0;
positions[i3 + 2] = branchAngle // z轴坐标
}
i % branches
意为当前粒子数对分支取余,假如branches = 4
,便可以始终得到0,1,2,3
,以此在若干趟遍历中控制只有branches
个相同的值;除以branches
,就可以获得比例如0,0.25,0.5,0.75
;最后再乘以2Π
即为所得(可以想像成一个圆上每90°取1个点)
按照上面的代码处理后会得到下面的效果,对应的4个点坐标为:0,0,0
、Π / 2,0,Π / 2
、Π,0,Π
、3Π / 2,0,3Π / 2
我们得到了角度值,但是仅仅是值,还没有将他和方向关联上,如果我们需要生成前后左右4个方向的直线,那他的值应该类似这样:
1,0,0
、0,0,1
、-1,0,0
、0,0,-1
这些值实际上就是x、z
轴值分别取余弦和正弦的结果:
cos(0),0,sin(0)
、cos(Π / 2),0,sin(Π / 2)
、cos(Π),0,sin(Π)
、cos(3Π / 2),0,sin(3Π / 2)
所以代码应该改为:
// ...
positions[i3] = Math.cos(branchAngle);
positions[i3 + 1] = 0;
positions[i3 + 2] = Math.sin(branchAngle);
这样就可以获取到下面的效果:
接着就是要实现粒子随机分布在这4个方向上,实际上需要的就是给每个粒子设置一个半径
的长度,这个长度可以使用Math.random()
获取:
const i3 = i * 3;
// 假设params.radius = 1
const radius = params.radius * Math.random();
const branchAngle = ((i % params.branches) / params.branches) * Math.PI * 2;
positions[i3] = Math.cos(branchAngle) * radius;
positions[i3 + 1] = 0;
positions[i3 + 2] = Math.sin(branchAngle) * radius;
这样就可以获得我们想要的效果了:
到此,就可以通过修改上面的变量和粒子数量、大小来调试出更多直线了,使用lil-gui
加上参数的调制,并且在每次调制完成后,重新生成整个”银河系“
const params = {
count: 100000, // 粒子数量
pointSize: 0.01,
radius: 5,
branches: 4,
};
gui
.add(params, "count")
.name("粒子数量")
.min(100)
.max(100000)
.step(100)
.onFinishChange(generateGalaxy);
gui
.add(params, "pointSize")
.name("粒子大小")
.min(0.001)
.max(0.2)
.step(0.001)
.onFinishChange(generateGalaxy);
gui
.add(params, "radius")
.name("半径")
.min(0.01)
.max(20)
.step(0.01)
.onFinishChange(generateGalaxy);
gui
.add(params, "branches")
.name("分支数")
.min(2)
.max(20)
.step(1)
.onFinishChange(generateGalaxy);
比如调整分支数到17条,得到的效果就是:
到这一步为止的完整代码:
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import GUI from "lil-gui";
// Debug
const gui = new GUI({
width: 400,
});
// Canvas
const canvas = document.querySelector("canvas.webgl");
// Scene
const scene = new THREE.Scene();
scene.add(new THREE.AxesHelper(30));
// 银河系
const params = {
count: 100000, // 粒子数量
pointSize: 0.01,
radius: 5,
branches: 4,
};
let geometry = null;
let material = null;
let points = null;
const generateGalaxy = () => {
if (points !== null) {
geometry.dispose();
material.dispose();
scene.remove(points);
}
geometry = new THREE.BufferGeometry();
material = new THREE.PointsMaterial({
size: params.pointSize,
sizeAttenuation: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
const positions = new Float32Array(params.count * 3);
for (let i = 0; i < params.count; i++) {
const i3 = i * 3;
const radius = params.radius * Math.random();
const branchAngle = ((i % params.branches) / params.branches) * Math.PI * 2;
positions[i3] = Math.cos(branchAngle) * radius;
positions[i3 + 1] = 0;
positions[i3 + 2] = Math.sin(branchAngle) * radius;
}
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
points = new THREE.Points(geometry, material);
scene.add(points);
};
generateGalaxy();
gui
.add(params, "count")
.name("粒子数量")
.min(100)
.max(100000)
.step(100)
.onFinishChange(generateGalaxy);
gui
.add(params, "pointSize")
.name("粒子大小")
.min(0.001)
.max(0.2)
.step(0.001)
.onFinishChange(generateGalaxy);
gui
.add(params, "radius")
.name("半径")
.min(0.01)
.max(20)
.step(0.01)
.onFinishChange(generateGalaxy);
gui
.add(params, "branches")
.name("分支数")
.min(2)
.max(20)
.step(1)
.onFinishChange(generateGalaxy);
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
};
window.addEventListener("resize", () => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
const camera = new THREE.PerspectiveCamera(
75,
sizes.width / sizes.height,
0.1,
100
);
camera.position.x = 3;
camera.position.y = 3;
camera.position.z = 3;
scene.add(camera);
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const clock = new THREE.Clock();
const tick = () => {
const elapsedTime = clock.getElapsedTime();
controls.update();
renderer.render(scene, camera);
window.requestAnimationFrame(tick);
};
tick();
每次调用generateGalaxy
方法会先销毁原本的几何体和材质并把原本的粒子全部移除,这样做既是为了重置画面,也为了节约内存开销,因为threejs
里的材质、几何体都需要手动清除
直线弯曲
接下来,需要对直线实现弯曲的效果;每条直线实际上是由若干个粒子以不同的长度
构成的,直线弯曲实际上就是让直线上的点发生一定量的偏移即可,但是要注意每个点偏移的量不能相同,不然只是相当于修改了直线的角度而已,比如:
positions[i3] = Math.cos(branchAngle + 1) * radius;
positions[i3 + 1] = 0;
positions[i3 + 2] = Math.sin(branchAngle + 1) * radius;
所以,每个粒子的偏移量应该由Math.random
生成,我们已经有了粒子的半径radius
,他就是一个随机量,每个粒子的半径都不同,所以代码可以写成:
positions[i3] = Math.cos(branchAngle + radius) * radius;
positions[i3 + 1] = 0;
positions[i3 + 2] = Math.sin(branchAngle + radius) * radius;
这样,基本的旋转的效果就实现了,然后我们可以绑定一个可控制的变量
到操作面板里,控制其螺旋的圈数:
for (let i = 0; i < params.count; i++) {
const i3= i * 3;
const radius = params.radius * Math.random();
const branchAngle = ((i % params.branches) / params.branches) * Math.PI * 2;
// 旋转圈数
const spinAngle = params.spin * radius;
positions[i3] = Math.cos(branchAngle + spinAngle) * radius;
positions[i3 + 1] = 0;
positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius;
}
gui
.add(params, "spin")
.name("旋转圈数")
.min(-5)
.max(5)
.step(0.001)
.onFinishChange(generateGalaxy);
颜色控制
要控制粒子的颜色,可以通过修改geometry
的顶点颜色来实现,也可以通过给PointsMaterial
设置color
属性实现
我们现在要实现的是给每个粒子设置不同的颜色,如果要通过PointsMaterial
去设置,这就意味着每次循环都要执行一次points = new Points(geomery, material)
才能实现,这样子对性能占用的比较严重,所以还是应该使用顶点着色
的方法实现:
const colors = new Float32Array(params.count * 3);
for (let i = 0; i < params.count; i++) {
colors[i3] = Math.random();
colors[i3 + 1] = Math.random();
colors[i3 + 2] = Math.random();
}
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
看着像是白色的一条线,但其实只是粒子太多且混合模式设置成了THREE.AdditiveBlending
导致,把粒子数量减少,放大可以看到颜色是作用到了每个顶点(粒子)的
完善形状
目前螺旋形状还只是一个平面,因为y
轴坐标还始终是0,给y轴坐标加上一个随机值让形状立体起来:
positions[i3] = Math.cos(branchAngle + spinAngle) * radius;
positions[i3 + 1] = (Math.random() - 0.5) * 2; // +
positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius;
但是目前的样子还是存在一定的规律性,可以试着再添加一点随机性;银河系的中间通常是一个大光圈,越到延申出来的尾部时光线越离散,所以可以认为粒子在中心密度高,尾部密度低,接下来就来实现这个需求
越靠近中心,意味着离原点越近,假如对其值取平方、三次方,那么靠近0的值将无限趋近于0,可以参考x^3
的函数图像来看:
所以,可以给粒子的x、y、z
的偏移量加上取幂指数的计算,这样就能实现我们的需求:
const randomX = Math.pow(Math.random(), 3)
const randomY = Math.pow(Math.random(), 3)
const randomZ = Math.pow(Math.random(), 3)
positions[i3] = Math.cos(branchAngle + spinAngle) * radius + randomX;
positions[i3 + 1] = randomY;
positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius + randomZ;
颜色渐变
目前的颜色机械质感严重,银河系的颜色通常是从中间到尾部的渐变色,所以接下来我们试着实现渐变色需求
首先我们用threejs
提供的颜色类创建一个中心颜色,再定义一个尾部的颜色,让threejs
自动计算从中心到尾部的颜色渐变
const colorInside = new THREE.Color("#ff6030");
const colorOutSide = new THREE.Color("#1b3984");
for (let i = 0; i < params.count; i++) {
// ...
const radius = params.radius * Math.random();
const mixedColor = colorInside.clone(); // +
mixedColor.lerp(colorOutSide, radius / params.radius); // +
colors[i3] = mixedColor.r; // +
colors[i3 + 1] = mixedColor.g; // +
colors[i3 + 2] = mixedColor.b; // +
}
首先将insideColor
通过颜色类内置的clone
方法进行一次深复制,然后调用颜色类内置的lerp
方法生成insideColor
到outsideColor
的渐变;radius
是每个粒子随机的半径,用它除以我们设置的基准半径就是粒子当前粒子所在的位置比例(注意这里不能直接使用Math.random()
)
threejs
中对颜色类提供的lerp
方法的解释:
最后,对新添加的变量使用lil-gui
接管就完成了一个可控制样式的银河系了
完整代码
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import GUI from "lil-gui";
/**
* Base
*/
// Debug
const gui = new GUI({
width: 400,
});
// Canvas
const canvas = document.querySelector("canvas.webgl");
// Scene
const scene = new THREE.Scene();
scene.add(new THREE.AxesHelper(30));
// 银河系
const params = {
count: 100000, // 粒子数量
pointSize: 0.01,
radius: 5,
branches: 4,
spin: 1,
randomness: 0.2,
randomnessPow: 3,
insideColor: "#ff6030",
outsideColor: "#1b3984",
};
let geometry = null;
let material = null;
let points = null;
const generateGalaxy = () => {
if (points !== null) {
geometry.dispose();
material.dispose();
scene.remove(points);
}
geometry = new THREE.BufferGeometry();
material = new THREE.PointsMaterial({
size: params.pointSize,
sizeAttenuation: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true,
});
const positions = new Float32Array(params.count * 3);
const colors = new Float32Array(params.count * 3);
const colorInside = new THREE.Color(params.insideColor);
const colorOutSide = new THREE.Color(params.outsideColor);
for (let i = 0; i < params.count; i++) {
const i3 = i * 3;
const radius = Math.random() * params.radius;
const branchAngle = ((i % params.branches) / params.branches) * Math.PI * 2;
const spinAngle = params.spin * radius;
const randomX =
Math.pow(Math.random(), params.randomnessPow) *
(Math.random() < 0.5 ? 1 : -1);
const randomY =
Math.pow(Math.random(), params.randomnessPow) *
(Math.random() < 0.5 ? 1 : -1);
const randomZ =
Math.pow(Math.random(), params.randomnessPow) *
(Math.random() < 0.5 ? 1 : -1);
positions[i3] = Math.cos(branchAngle + spinAngle) * radius + randomX;
positions[i3 + 1] = randomY;
positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius + randomZ;
const mixedColor = colorInside.clone();
mixedColor.lerp(colorOutSide, radius / params.radius);
colors[i3] = mixedColor.r;
colors[i3 + 1] = mixedColor.g;
colors[i3 + 2] = mixedColor.b;
}
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
points = new THREE.Points(geometry, material);
scene.add(points);
};
generateGalaxy();
gui
.add(params, "count")
.name("粒子数量")
.min(100)
.max(100000)
.step(100)
.onFinishChange(generateGalaxy);
gui
.add(params, "pointSize")
.name("粒子大小")
.min(0.001)
.max(0.2)
.step(0.001)
.onFinishChange(generateGalaxy);
gui
.add(params, "radius")
.name("半径")
.min(0.01)
.max(20)
.step(0.01)
.onFinishChange(generateGalaxy);
gui
.add(params, "branches")
.name("分支数")
.min(2)
.max(20)
.step(1)
.onFinishChange(generateGalaxy);
gui
.add(params, "spin")
.name("旋转幅度")
.min(-5)
.max(5)
.step(0.001)
.onFinishChange(generateGalaxy);
gui
.add(params, "randomnessPow")
.name("指数幂")
.min(1)
.max(10)
.step(0.001)
.onFinishChange(generateGalaxy);
gui
.addColor(params, "insideColor")
.name("内部颜色")
.onFinishChange(generateGalaxy);
gui
.addColor(params, "outsideColor")
.name("外部颜色")
.onFinishChange(generateGalaxy);
/**
* Sizes
*/
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
};
window.addEventListener("resize", () => {
// Update sizes
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
// Update camera
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
// Update renderer
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
/**
* Camera
*/
// Base camera
const camera = new THREE.PerspectiveCamera(
75,
sizes.width / sizes.height,
0.1,
100
);
camera.position.x = 3;
camera.position.y = 3;
camera.position.z = 3;
scene.add(camera);
// Controls
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
/**
* Renderer
*/
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
/**
* Animate
*/
const clock = new THREE.Clock();
const tick = () => {
const elapsedTime = clock.getElapsedTime();
// Update controls
controls.update();
// Render
renderer.render(scene, camera);
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
tick();