浏览器对 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 = 0xfffffffunction 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} />}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: THREE.Vector3[] = [] 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