Threejs examples
浏览器对 WebGL Contexts
数量有要求,数量过多会将旧的删除,因此单独分一个 example 文章:
为了方便创建渲染器,这里写了一个 buildRenderer
、buildCamera
函数:
第一个例子没有使用这个函数,因为我觉得注释对理解很有必要。
import { type RefObject } from 'react'
import * as THREE from 'three'
export function buildRenderer(ref: RefObject<HTMLDivElement>) {
const container = ref.current!
const size = container.clientWidth
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(size, size)
renderer.pixelRatio = window.devicePixelRatio
container.appendChild(renderer.domElement)
return renderer
}
export function buildCamera(x: number, y: number, z: number) {
const fov = 45
const aspect = 1
const near = 0.1
const far = 1000
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far)
camera.position.set(x, y, z)
camera.lookAt(0, 0, 0)
return camera
}
Rotating cube
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
function FirstScence() {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const container = containerRef.current!
const size = container.clientWidth
// 1. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
// 设置背景色透明
renderer.setClearAlpha(0)
// 指定设备像素密度
renderer.pixelRatio = window.devicePixelRatio
// 设置大小
renderer.setSize(size, size)
// 将渲染器的 dom 节点加入 container 下
container.appendChild(renderer.domElement)
// 2. 创建场景
const scene = new THREE.Scene()
// 2.1 创建几何体
const geometry = new THREE.BoxGeometry(4, 4, 4)
// 2.2 创建材质
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
// 2.3 集合体和材质组合成 Mesh 对象
const cube = new THREE.Mesh(geometry, material)
// 2.4 将 Mesh 对象添加到场景中
scene.add(cube)
// 2.5 旋转动画
function animate() {
requestAnimationFrame(animate)
cube.rotation.y += 0.01
renderer.render(scene, camera)
}
// 3. 创建相机
const camera = new THREE.PerspectiveCamera(
75, // fov
1 / 1, // aspect
0.1, // near
1000, // far
)
// 设置相机的位置 (x, y, z) = (5, 5, 10)
camera.position.set(5, 5, 10)
// 设置相机看向原点 0, 0, 0
camera.lookAt(0, 0, 0)
// 4. 创建物体
const axis = new THREE.AxesHelper(5)
// 添加到场景中
scene.add(axis)
// 5. 渲染
const cancleFaf = animate() // renderer.render(scene, camera)
return cancleFaf
}, [])
return <div ref={containerRef} />
}
export default FirstScence
如何确保自转时间?上面的例子中,使用的是 requestAnimationFrame
api,调用频率与用户的浏览器刷新率有关,不同硬件、浏览器环境下,自转的速度也不一样,为了使得旋转速度一致,我们可以使用 Clock
解决:
const clock = new Three.Clock()
function animate() {
requestAnimationFrame(animate)
const elapsedTime = clock.getElapsedTime()
cube.rotation.y = elapsedTime * Math.PI
renderer.render(scene, camera)
}
Controls
OrbitControls
允许我们对场景内容进行旋转、放大缩小等操作。
注意,如果使用之前的
animate
函数不断调用渲染器的render
方法渲染的,是不需要为OrbitControls
添加change
事件的。
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/Addons.js'
import { buildCamera, buildRenderer } from './utils'
function ThreeControlPureFirstScene() {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const renderer = buildRenderer(containerRef)
const scene = new THREE.Scene()
const geometry = new THREE.BoxGeometry(4, 4, 4)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
function render() {
renderer.render(scene, camera)
}
const camera = buildCamera(5, 5, 10)
const controls = new OrbitControls(camera, renderer.domElement)
controls.update()
controls.addEventListener('change', render)
const axis = new THREE.AxesHelper(5)
scene.add(axis)
render()
return () => {
controls.removeEventListener('change', render)
}
}, [])
return <div ref={containerRef} />
}
export default ThreeControlPureFirstScene
Light
MeshBasicMaterial
不受光源影响,需要设置为 MeshStandardMaterial
。
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/Addons.js'
import { buildCamera, buildRenderer } from './utils'
const White = 0xffffff
function LightPureFirstScence() {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const renderer = buildRenderer(containerRef)
// 设置渲染器渲染阴影
renderer.shadowMap.enabled = true
const scene = new THREE.Scene()
const geometry = new THREE.BoxGeometry(4, 4, 4)
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 })
material.emissive = new THREE.Color(0x48211a)
const cube = new THREE.Mesh(geometry, material)
// 表示 cube 可以投射阴影
cube.castShadow = true
// 平面几何
const planeGeometry = new THREE.PlaneGeometry(20, 20)
const planeMaterial = new THREE.MeshStandardMaterial({ color: White })
// 材质的放射光颜色,不受其他光照影响的固有颜色,默认为黑色
planeMaterial.emissive = new THREE.Color(0x444444)
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial)
planeMesh.rotation.x = -0.5 * Math.PI
planeMesh.position.set(0, -6, 0)
// 接受投射的阴影
planeMesh.receiveShadow = true
// 方向光
const directionalLight = new THREE.DirectionalLight(White, 0.5)
// 设置方向(根据源点设置)
directionalLight.position.set(15, 15, 15)
// 表示该方向会投射阴影效果
directionalLight.castShadow = true
scene.add(directionalLight)
scene.add(planeMesh)
scene.add(cube)
function render() {
renderer.render(scene, camera)
}
const axis = new THREE.AxesHelper(5)
scene.add(axis)
const camera = buildCamera(15, 15, 15)
const controls = new OrbitControls(camera, renderer.domElement)
controls.addEventListener('change', render)
controls.update()
render()
return () => {
controls.removeEventListener('change', render)
}
}, [])
return <div ref={containerRef}></div>
}
export default LightPureFirstScence
Drawing lines
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/Addons.js'
import { buildCamera, buildRenderer } from './utils'
function PureLine() {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const renderer = buildRenderer(containerRef)
const material = new THREE.LineBasicMaterial({ color: 0xff0000 })
const points = []
points.push(
...[
new THREE.Vector3(-5, 0, 0),
new THREE.Vector3(0, 5, 0),
new THREE.Vector3(5, 0, 0),
],
)
const geometry = new THREE.BufferGeometry().setFromPoints(points)
const line = new THREE.Line(geometry, material)
const camera = buildCamera(10, 10, 10)
const scene = new THREE.Scene()
const controls = new OrbitControls(camera, renderer.domElement)
controls.addEventListener('change', render)
controls.update()
scene.add(line)
function render() {
renderer.render(scene, camera)
}
render()
return () => {
controls.removeEventListener('change', render)
}
}, [])
return <div ref={containerRef} />
}
export default PureLine
Box with edge
import { useRef, useEffect } from 'react'
import * as THREE from 'three'
import { buildCamera, buildRenderer } from './utils'
function ThreeLearnPrimitivesBox() {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const renderer = buildRenderer(containerRef)
const scene = new THREE.Scene()
const camera = buildCamera(10, 10, 10)
const box = new THREE.BoxGeometry(4, 4, 4)
const material = new THREE.MeshBasicMaterial({ color: 0x3451b2 })
const mesh = new THREE.Mesh(box, material)
// 获取 box 的边界
const edges = new THREE.EdgesGeometry(box)
const edgesMaterial = new THREE.LineBasicMaterial({
color: 0x00ffff,
})
const line = new THREE.LineSegments(edges, edgesMaterial)
mesh.add(line)
scene.add(mesh)
function render() {
renderer.render(scene, camera)
}
function animate() {
requestAnimationFrame(animate)
mesh.rotation.x += 0.01
mesh.rotation.y += 0.01
mesh.rotation.z += 0.01
render()
}
const cancelRaf = animate()
return cancelRaf
}, [])
return <div ref={containerRef} />
}
export default ThreeLearnPrimitivesBox
Render Text
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js'
import { FontLoader, OrbitControls } from 'three/examples/jsm/Addons.js'
import { buildCamera, buildRenderer } from './utils'
function PureText() {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const fontLoadr = new FontLoader()
fontLoadr.load('/threejs/fonts/gentilis_regular.typeface.json', (font) => {
const renderer = buildRenderer(containerRef)
const scene = new THREE.Scene()
const camera = buildCamera(0, 0, 10)
const material = new THREE.MeshStandardMaterial({
color: 0xff0000,
})
material.emissive = new THREE.Color(0x48211a)
const geometry = new TextGeometry('Hello', {
font,
size: 24,
depth: 24,
})
const light = new THREE.DirectionalLight(0xffffff, 0.4)
light.position.set(1, 1, 1)
light.castShadow = true
const mesh = new THREE.Mesh(geometry, material)
mesh.castShadow = true
scene.add(mesh)
scene.add(light)
const controls = new OrbitControls(camera, renderer.domElement)
controls.update()
renderer.render(scene, camera)
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
}
const cancelRaf = animate()
return cancelRaf
})
}, [])
return <div ref={containerRef} />
}
export default PureText
Loading 3D modales
Three.js 支持很多导入工具,官网推荐 glTF(gl 传输格式),这种格式传输效率和加载速度都非常优秀,而且也包括了网格、材质、纹理、皮肤、骨骼、变形目标、动画、灯光和摄像机等,很多工具都包含了 glTF 导出功能:
three.js 内置了 GLTFLoader
,可以载入 glTF
模型。
另外一些环境贴图加载器也是必须的,例如 RGBELoader
可以加载高动态范围(HDR)环境贴图,通常用于创建更逼真的光照和反射效果,详见 wiki。
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
import {
GLTFLoader,
OrbitControls,
RGBELoader,
} from 'three/examples/jsm/Addons.js'
import { buildCamera, buildRenderer } from './utils'
function ThreePureModel() {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const renderer = buildRenderer(containerRef)
const scene = new THREE.Scene()
const camera = buildCamera(-1.8, 0.6, 2.7)
function render() {
renderer.render(scene, camera)
}
function renderModal() {
const loader = new GLTFLoader()
loader.setPath('/threejs/models/glTF/DamagedHelmet/')
loader.load(
'DamagedHelmet.gltf',
async (gltf) => {
const model = gltf.scene
await renderer.compileAsync(model, camera, scene)
scene.add(model)
render()
},
undefined,
(error) => {
console.log(error)
},
)
}
new RGBELoader().load(
'/threejs/textures/royal_esplanade_1k.hdr',
(texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping
scene.background = texture
scene.environment = texture
renderModal()
},
)
const controls = new OrbitControls(camera, renderer.domElement)
controls.addEventListener('change', render)
controls.update()
return () => {
controls.removeEventListener('change', render)
}
}, [])
return <div ref={containerRef} />
}
export default ThreePureModel
Sun Earth and moon
这个 demo 的关键是了解 three,js 的场景图。场景图在 3D 引擎是一个图中节点的层次结构,每个节点代表了一个局部空间(local space):
例如下面系统的场景图:
可能很多人有疑问:为什么需要单独设置 solarSystem
、earthOrbit
和 moonOrbit
?至少第一次我认为下面的场景图更加简洁:
这里的重点在于局部空间,参考下面第 22、25 行代码,如果我们将 sun
放大五倍,作为子节点的 earth
也会放大五倍,同理 moon
也会放大五倍,为了避免缩放之间互相影响,我们使用了 Object3D
将对象进行了组合,这样每个图元就不会互相影响了。
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
import { buildCamera, buildRenderer } from './utils'
function ThreeSunEarthMoon() {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const renderer = buildRenderer(containerRef)
const camera = buildCamera(0, 35, 0)
// 参数:https://threejs.org/manual/#zh/primitives#Diagram-SphereGeometry
const sphereGeometry = new THREE.SphereGeometry(1, 6, 6)
const sunMaterial = new THREE.MeshPhongMaterial({ emissive: 0xffff00 })
const earthMaterial = new THREE.MeshPhongMaterial({
emissive: 0x000ff,
color: 0x2233ff,
})
const moonMaterial = new THREE.MeshPhongMaterial({ emissive: 0x888888 })
const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial)
sunMesh.scale.set(4, 4, 4)
const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial)
const moonMesh = new THREE.Mesh(sphereGeometry, moonMaterial)
moonMesh.scale.set(0.5, 0.5, 0.5)
const scene = new THREE.Scene()
const solarSystem = new THREE.Object3D()
const earthOrbit = new THREE.Object3D()
earthOrbit.position.x = 10
const moonOrbit = new THREE.Object3D()
moonOrbit.position.x = 2
const light = new THREE.PointLight(0xffffff, 1500)
scene.add(light)
scene.add(solarSystem)
solarSystem.add(sunMesh)
solarSystem.add(earthOrbit)
earthOrbit.add(earthMesh)
earthOrbit.add(moonOrbit)
moonOrbit.add(moonMesh)
const points = [solarSystem, sunMesh, earthOrbit, earthMesh, moonOrbit]
function animate() {
requestAnimationFrame(animate)
points.forEach((point) => {
point.rotation.y += 0.015
})
renderer.render(scene, camera)
}
const cancelRaf = animate()
return cancelRaf
}, [])
return <div ref={containerRef} />
}
export default ThreeSunEarthMoon