更新于 

Three.js Tutorial for Absolute Beginners

源项目仓库:

npm安装

  • parcel.js 程序打包工具
  • three.js 图表库
    • threejs尽量安装稳定版本,最新版本可能存在各种问题
1
2
npm install three@0.133.0
npm install parcel
parcel.js

parcel将多个源文件打包成一个dist,
支持开发时热更新。

文件结构如下时:

  • root
    • src
      • js
        • main.js
      • index.html 主页面入口
    • package.json

在项目根目录路径root\下使用parcel命令:

1
parcel index.html

parcel会在项目根目录root\下创建一个dist文件夹,
即真正运行的文件

渲染器Renderer/场景Scene/摄像机Camera

场景Scene
1
const scene = new THREE.Scene()
摄像机Camera
1
2
3
4
5
6
7
8
9
const camera = new THREE.PerspectiveCamera(
75, //fov
window.innerWidth / window.innerHeight, //aspect
0.01, //near
10000 //far
)
scene.add(camera)
camera.position.set(20, 20, 20)
camera.lookAt(0,0,0)
Renderer渲染器
1
2
3
4
const renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.append(renderer.domElement)
renderer.render(scene, camera)

坐标系辅助线AxesHelper/物体Object/动画Animate/轨道控制器OrbitControls

AxesHepler
1
2
const axesHelper = new THREE.AxesHelper(15)
scene.add(axesHelper)
Object
1
2
3
4
5
6
const boxGeometry = new THREE.BoxGeometry(3,3,3)
const boxMetarial = new THREE.MeshBasicMaterial({
color:0x00ff00
})
const box = new THREE.Mesh(boxGeometry, boxMetarial)
scene.add(box)
Animate

记得每次调用animate时都要调用render进行重绘

1
2
3
4
5
6
renderer.setAnimationLoop(animate)
function animate(time) {
box.rotation.x = time / 1000
box.rotation.y = time / 1000
renderer.render(scene, camera)
}
OrbitControls

每次对相机进行修改后,都需要调用orbit.update()进行更新

1
2
3
4
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls'
// ...
const orbit = new OrbitControls(camera, renderer.domElement)
orbit.update()

Geometry几何构造/Material材质/GridHelper网格辅助器

Geometry&&Material
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// plane geometry
const planeGeometry = new THREE.PlaneGeometry(30, 30)
const planeMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
side:THREE.DoubleSide
})
const plane = new THREE.Mesh(planeGeometry, planeMaterial)
scene.add(plane)
plane.rotation.x = -Math.PI / 2
// sphere geometry
const sphereGeometry = new THREE.SphereGeometry(4)
const sphereMaterial = new THREE.MeshBasicMaterial({
color:0xffff00
})
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
scene.add(sphere)
sphere.position.set(-10,10,10)
GridHelper

GridHelper:

  • param1: size
  • param2: divisions
    1
    2
    const gridHelper = new THREE.GridHelper(30, 30)
    scene.add(gridHelper)

dat.gui交互界面

npm安装dat.gui

1
npm install dat.gui
dat.gui
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { GUI } from 'dat.gui'
const options = {
sphereColor: 0xb16c4b,
wireframe: false,
speed: 0.05
}
const gui = new GUI()
gui.addColor(options, 'sphereColor').onChange((e) => {
sphere.material.color.set(e)
})
gui.add(options, 'wireframe').onChange(e => {
sphere.material.wireframe = e
})
gui.add(options, 'speed', 0, 0.1)
1
2
3
4
5
function animate(time) {
// ...
sphere.position.y = 10*Math.abs(Math.sin(Math.PI*(time/100)*options.speed))
renderer.render(scene, camera)
}

AmbientLight环境光/DirectionalLight直射光/SpotLight聚光灯/Shadow阴影

将之前创建的物体材质改成受光照影响的MeshStandardMaterial

AmbientLight
1
2
const ambientLight = new THREE.AmbientLight(0xffffff)
scene.add(ambientLight)
DirectionalLight
1
2
3
4
5
const directionalLight = new THREE.DirectionalLight(0xffffff)
scene.add(directionalLight)
directionalLight.position.set(-30,30,30)
const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight ,5, 0x0000ff)
scene.add(directionalLightHelper)

DirectionalLight的阴影底部范围默认为5,
需要通过改变light.shadow.camera.bottom来控制底部范围

1
directionalLight.shadow.camera.bottom = -10
Shadow

要投射阴影,首先要将renderer的shadowMap属性打开

1
renderer.shadowMap.enabled = true

然后需要将光源的castShadow属性打开,
需要投射阴影物体的castShadow设为true,
被投射阴影的面的receiveShadow设为true

1
2
3
directionalLight.castShadow = true
sphere.castShadow = true
plane.receiveShadow = true
SpotLight
1
2
3
4
5
6
const spotLight = new THREE.SpotLight(0xffffff)
scene.add(spotLight)
spotLight.position.set(-30, 30, 30)
spotLight.castShadow = true
const spotLightHelper = new THREE.SpotLightHelper(spotLight)
scene.add(spotLightHelper)

Fog雾/SceneBackground场景背景/Texture纹理

Fog

方式1:指定雾的范围

1
scene.fog = new THREE.Fog(0xffffff,20, 200)

方式2:指定雾的密度

1
scene.fog = new THREE.FogExp2(0xffffff,0.01)
textureLoader

单个textureLoader:

1
2
3
import nebula from '../img/nebula.jpg'
const textureLoader = new THREE.TextureLoader()
scene.background = textureLoader.load(nebula)

cubeTextureLoader:

1
2
3
4
5
6
7
8
9
10
11
import starts from '../img/stars.jpg'
import nebula from '../img/nebula.jpg'
const cubeTextureLoader = new THREE.CubeTextureLoader()
scene.background = cubeTextureLoader.load([
nebula,
starts,
starts,
nebula,
nebula,
starts
])
Texture

物体添加纹理:

1
2
3
4
5
6
7
8
import nebula from '../img/nebula.jpg'
const textureLoader = new THREE.TextureLoader()
const boxGeometry2 = new THREE.BoxGeometry(3,3,3)
const boxMetarial2 = new THREE.MeshBasicMaterial({
map:textureLoader.load(nebula)
})
const box2 = new THREE.Mesh(boxGeometry2, boxMetarial2)
scene.add(box2)

同一个物体添加多个材质:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import starts from '../img/stars.jpg'
import nebula from '../img/nebula.jpg'
const textureLoader = new THREE.TextureLoader()
const boxGeometry2 = new THREE.BoxGeometry(3,3,3)
const boxMetarial2 = [
new THREE.MeshBasicMaterial({map:textureLoader.load(nebula)}),
new THREE.MeshBasicMaterial({map:textureLoader.load(nebula)}),
new THREE.MeshBasicMaterial({map:textureLoader.load(starts)}),
new THREE.MeshBasicMaterial({map:textureLoader.load(nebula)}),
new THREE.MeshBasicMaterial({map:textureLoader.load(nebula)}),
new THREE.MeshBasicMaterial({map:textureLoader.load(starts)}),
]
const box2 = new THREE.Mesh(boxGeometry2, boxMetarial2)
scene.add(box2)

Select选中

selection

选中一个物体,首先需要在绘制区域创建一个二维坐标系:

1
2
3
4
5
let mousePosition = new THREE.Vector2()
window.onmousemove = function (e) {
mousePosition.x = (e.clientX / window.innerWidth * 2) - 1
mousePosition.y = 1 - (e.clientY / window.innerHeight * 2)
}

这里实现选中物体的原理是从摄像机Camera和鼠标Mouse之间发射出一条射线,
所有被射线穿透的物体即视为选中状态,

因此,我们需要创建一条射线:

1
let rayCaster = new THREE.Raycaster()

然后在loop函数中持续对选中物体进行检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function animate(time) {
// ...
rayCaster.setFromCamera(mousePosition, camera)
let intersects = rayCaster.intersectObjects(scene.children)
for (let obj of intersects) {
if (obj.object.id === sphere.id) {
obj.object.material.color.set(0xffff00)
} else if (obj.object.name === box2.name) {
obj.object.rotation.x += 0.01
obj.object.rotation.z += 0.01
}
}
renderer.render(scene, camera)
}

VertexPosition顶点坐标/ShaderMaterial渲染器材质/ModelImport模型导入

Vertext.attribute.position

通过直接修改obj.geometry.attributes.position数组中数值进行顶点修改,
注意:修改后需要将needsUpdate设置为true

1
2
3
4
5
6
7
8
let posArray = plane.geometry.attributes.position.array;
for (let i = 0; i < posArray.length; i++) {
if (i % 3 == 2) {
let extent = (Math.abs(i-posArray.length/2)/posArray.length)*5
posArray[i] = (2+extent)*Math.sin(Math.PI*time/1000/Math.max(extent,i/posArray.length*5))
}
}
plane.geometry.attributes.position.needsUpdate = true
ShaderMaterial

方式1:shader程序用字符串变量/常量定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const vShader = `
void main(){
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`
const fShader = `
void main(){
gl_FragColor = vec4( 0.7, 0.5, 1.0, 1.0);
}
`
const sphereMaterial = new THREE.ShaderMaterial({
vertexShader: vShader,
fragmentShader: fShader
})

方式2:shader程序使用script标签写在html文件中:

1
2
3
4
5
6
7
8
9
10
<script id="vShader" type="x-vertex">
void main(){
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
</script>
<script id="fShader" type="x-fragment">
void main(){
gl_FragColor = vec4( 0.7, 0.5, 1.0, 1.0);
}
</script>
1
2
3
4
const sphereMaterial = new THREE.ShaderMaterial({
vertexShader: document.getElementById('vShader').textContent,
fragmentShader: document.getElementById('fShader').textContent,
})
import model

这里以导入gltf格式模型为例,
对于不同模型,threejs提供了不同loader:

1
2
import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader'
const gltfLoader = new GLTFLoader()

由于项目内使用了parcel作为打包工具,
因此模型文件的路径需要动态获取:

1
const monkeyUrl = new URL('../assets/monkey.glb', import.meta.url)

通过loader读取模型数据并绘制:

1
2
3
4
5
6
7
8
gltfLoader.load(monkeyUrl.href, function (gltf) { 
let model = gltf.scene
scene.add(model)
model.position.set(0, 10, 0)
model.scale.set(2,2,2)
}, undefined, function (err) {
console.err(err)
})

CanvasResponsive响应式画布

onresize

在屏幕尺寸发生变化时,需要对相机camera的比例进行更新,
renderer需要重绘:

1
2
3
4
5
6
window.onresize = function () { 
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth,window.innerHeight)
}