threeJs学习笔记(四)

粒子

主要通过threejs里的PointsPointsMaterial实现

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

同样的,粒子材质也可以设置纹理,通过mapalphaMap等属性加载,不过通常会选择使用alphaMap进行加载,需要提供一个黑色背景,内容白色的图片:
在这里插入图片描述
比如下面的例子中,使用这张图片:
在这里插入图片描述

声明材质时,使用alphaMaptransparent

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,
});

在这里插入图片描述

上述alphaTestdepthTestdepthWrite都属于解决方案,没有完美的解决方案,但是通常对于粒子的场景都会使用DepthWrite: false,也可以三者结合使用弄出更好的效果

Blending

(颜色)混合模式,即设置场景中物体重合时的展现形式,可以参考threejs官方提供例子来理解,PS、PR等软件里也是有这种配置的,它们是同一个东西
在这里插入图片描述
在粒子的场景中,常用的是THREE.NormalBlendingTHREE.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,所以也可以通过在每一帧中修改rotationposition等属性来实现动画,比如实现一个粒子的旋转:

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;最后再乘以即为所得(可以想像成一个圆上每90°取1个点)

按照上面的代码处理后会得到下面的效果,对应的4个点坐标为:0,0,0Π / 2,0,Π / 2Π,0,Π3Π / 2,0,3Π / 2
在这里插入图片描述
我们得到了角度值,但是仅仅是值,还没有将他和方向关联上,如果我们需要生成前后左右4个方向的直线,那他的值应该类似这样:
1,0,00,0,1-1,0,00,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方法生成insideColoroutsideColor的渐变;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();

好的,我会为您解答关于WebGL three.js的阴影与实现物体动画的问题。首先,让我们来了解一下WebGL three.js是什么。 WebGL three.js是一款基于WebGL的JavaScript 3D库,可以帮助我们快速搭建3D场景和应用。接下来我们来讲解阴影和实现物体动画的方法。 一、阴影 阴影是模拟物体之间的阴影效果,让3D场景更加真实。在three.js中,我们可以通过设置Mesh的castShadow和receiveShadow属性来实现阴影效果。 1. 首先,我们需要在场景中添加光源,例如SpotLight或DirectionalLight。 2. 然后,在需要投射阴影的物体上设置castShadow为true。 3. 最后,在需要接收阴影的物体上设置receiveShadow为true。 代码示例: ```javascript // 添加光源 const light = new THREE.SpotLight(0xffffff); light.position.set(0, 100, 0); light.castShadow = true; scene.add(light); // 添加需要投射阴影的物体 const cube = new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshLambertMaterial({ color: 0xff0000 })); cube.castShadow = true; scene.add(cube); // 添加需要接收阴影的物体 const plane = new THREE.Mesh(new THREE.PlaneGeometry(200, 200, 1, 1), new THREE.MeshLambertMaterial({ color: 0xffffff })); plane.receiveShadow = true; plane.rotation.x = -Math.PI / 2; scene.add(plane); ``` 二、物体动画 在three.js中,我们可以通过Tween.js库来实现物体的动画效果。Tween.js是一款JavaScript动画库,可以帮助我们实现非常丰富的动画效果。 1. 首先,我们需要在HTML文件中引入Tween.js库文件。 2. 然后,在需要动画的物体上设置初始状态。 3. 最后,通过Tween.js库来设置物体的目标状态和动画效果,例如缓动动画(ease)或弹跳动画(bounce)。 代码示例: ```javascript // 引入Tween.js库文件 <script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.min.js"></script> // 添加需要动画的物体 const cube = new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshLambertMaterial({ color: 0xff0000 })); cube.position.set(0, 0, 0); scene.add(cube); // 设置初始状态 const start = { x: 0, y: 0, z: 0 }; // 设置目标状态 const end = { x: 50, y: 50, z: 50 }; // 设置动画效果 const tween = new TWEEN.Tween(start) .to(end, 1000) .easing(TWEEN.Easing.Quadratic.InOut) .onUpdate(() => { cube.position.set(start.x, start.y, start.z); }) .start(); ``` 以上是关于WebGL three.js阴影与实现物体动画的方法,希望能够对您有所帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值