前言
众所周知一个前端如果无聊到一定程度一定会去干些使命感强烈的活,不外乎读源码,造轮子,然后发展到读不进源码,意识到轮子是重复劳作,接着就开始新的邻域,例如three.js。
介绍
今天想分享的是一点点对于three的初见,本人学了一周,请高手指点。
什么是three.js
通常前端总是表单,列表,按钮等类似对现实世界的概念抽象,而不是直接的与现实世界的沟通,但three.js则更像是给前端一个与现实世界沟通的渠道。
概念
现实世界中,人眼观察世界的客观条件有:
- 光:
- 光类型(环境光,点光)
- 颜色
- 位置
- 物体:
- 材质
- 透明(或许可以包含在材质)
- 纹理
- 位置
- 观察者:
- 观察角度
- 视角范围
- 位置
由此三点我们构成了一个真正的场景,但要注意不是一定需要他们中的每一个都在才能组成,例如无光也可以,只要物体能发出颜色依旧可以看到(现实世界当然需要光才能让物体反射表面的颜色啦。。),因此需要灵活运用。 但我们依旧需要理解这些概念,以确保在接下来的代码中我们能好理解为什么要这么写。
从一个点开始
先上效果图:
没错,就是一个点。
接下来的代码,我们默认环境配置是vite+vue3,且已经安装了three.js(0.159.0)。
首先我们要声明一个场景:
Typescript
const scene = new THREE.Scene()
接着我们需要实例化一个几何体(本例我们暂不会严格对照上面的概念),用于给小点一个放置空间:
Typescript
const geometry = new THREE.BufferGeometry();
紧接着我们要给这个几何体一些小点放置位置:
Typescript
geometry.setAttribute('position', new THREE.Float32BufferAttribute([0,0,0], 3));
有了小点放置位置,我们就应该给小点一些属性,去描述它具体的样子(材质)。
typescript
const shader = new THREE.ShaderMaterial({
uniforms: {
uColor: {
value: new THREE.Color('#FFFF00')
},
uSzie: {
value: 200
}
},
vertexShader: `
uniform float uSzie;
void main() {
// 顶点着色器计算后的Position
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * mvPosition;
// 大小
gl_PointSize = uSzie * 300.0 / (-mvPosition.z);
}`,
fragmentShader: `
uniform vec3 uColor;
void main() {
if (distance(gl_PointCoord, vec2(0.5, 0.5)) > 0.5) discard;
gl_FragColor = vec4(uColor, 1.0);
}`
`
})
这一段代码中,不光有ts,还有一些由纯字符串组成的glsl代码(也俗称shader),这个我们会在后续详解,但我们需要知道给这个ShaderMaterial传递的属性有什么用。
属性 | 解释 |
---|---|
uniform | 可以理解为是用于给shader代码传递参数的 |
vertexSahder | 点着色器,用于描述材质点位置 |
fragmentShader | 面着色器,用于描述面颜色 |
接着我们只要融合位置与材质即可得到一个物体。
Typescript
const point = new THREE.Points(geometry, shader);
最后我们终于可以把写好的物体放进场景了。
Typescript
scene.add(point);
但不要忘了最重要的部分,观察者,在three中观察者的概念是camera下面的代码不多,且都易于理解,就直接贴上了。
Typescript
const sizes = {
width: window.innerWidth,
height: window.innerHeight
}
const camera = new THREE.PerspectiveCamera(45, sizes.width / sizes.height, 1, 10000)
camera.position.set(0, 0, 1200)
scene.add(camera)
其中需要注意camera的参数含义:
参数 | 解释 |
---|---|
45 | 视角宽,以人眼的视角为例子,大约有180度以上,但由于我们是在电脑前坐着,因此应当减少掉显示屏宽外的角度 |
sizes.width / sizes.height | 很容易理解,就是比例 |
1 | 简单理解就是最近的观察距离 |
10000 | 简单理解就是最远的观察距离 |
如果不能理解可以去看看下面的图,实在不能理解,直接不写这俩属性也可以。
接着我们设置了相机位置,其中三个数字不难理解为坐标轴上的某个点的坐标。 最后添加到场景中。
最后,我们还需要一个渲染器,来显示告诉three,我们要把什么场景渲染到哪个元素上。
Typescript
const renderer = new THREE.WebGLRenderer({
canvas: canvas as HTMLCanvasElement,
antialias: true,
alpha: true
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
以上代码我们新建了一个渲染器。
其中第一个参数为canvas的dom元素(注意使用vue或其他框架一定要在dom渲染后的生命周期使用)。
antialias为抗锯齿(为了让物体表面更圆滑,实际就是给一个比较合理的边缘模糊)。
alpha为透明,可以让canvas显示在其他元素上方。
这里仅举部分例,还有很多属性可选,但大多数情况写上dom就够用了。
下面的设置尺寸和设置像素比率都是固定写法。
额外的
为了增加互动,通常情况下我们还会让相机能够移动。
Typescript
const controls = new OrbitControls(camera, canvas as HTMLElement)
最后,如果我们写了会多次渲染的场景,例如shader传递的参数里有会时间或其他会改变的输入,又或者我们手动更改了任何东西的位置(这个观察器也算)等等,我们必须及时更新最新的渲染画面
Typescript
const tick = () => {
controls.update()
renderer.render(scene, camera)
requestAnimationFrame(tick)
}
tick()
这是一段简单的动画执行代码,其中包含了更新控制器,更新画面,且每16毫秒再次更新。 关于 requestAnimationFrame 可以查看我的笔记了解效果图 (bcomponent.vercel.app)。
至此我们完整的结束了使用three.js来画一个最基本的小点。千里之行始于足下,这个小点将启发我们更多的可能Vite + Vue + TS (learn3d-neon.vercel.app)。
写在最后
感谢大家观看,后续我会继续分享three的其他动画以及如何将shadertoy中的大海搬到我们自己的页面中。