[Three.js-04] Three.js 应用的基本组成
这是我的学习总结,水平有限,如有错误或不妥的地方还希望大家能够指出来 (^_^)respect! 另外我自己收集的【webgl\three.js\图形学】相关的关资源我已经放在微信群里(WX: Galloping_Leo )面了,需要的话,加群,一起学习共同进步。
本篇是第一篇的延续,旨在建立一个对 three.js 的基本认识,知道基本概念和简单使用,后续会更详细地进行分享,一步一步来,不积跬步无以至千里。
Scene
THREE.Scene 是网格对象(mesh)和光线等等的容器,在 Three.js 中 Scene 中包含所有需要渲染的对象。THREE.Secene 没有太多的属性和方法。Scene 它是一个树状结构,由很多对象组成,比如图中包含了一个场景(Scene)对象 ,多个网格(Mesh)对象,光源(Light)对象,群组(Group),三维物体(Object3D),和摄像机(Camera)对象。一个场景(Scene)对象定义了场景图最基本的要素,并包了含背景色和雾等属性。这些对象通过一个层级关系明确的树状结构来展示出各自的位置和方向。子对象的位置和方向总是相对于父对象而言的。比如说汽车的轮子是汽车的子对象,这样移动和定位汽车时就会自动移动轮子。
Three.Scene
继承自 Three.Object3D
,所有继承 Object3D
的类都有如下的方法:
-
add 方法
scene.add(...object: Object3D[])
向场景中加入对象。如果一个对象 a 已经存在于 A 对象之下,现在存在 B 对象与 A 对象之间不存在父子关系,执行B.add(a)
,a 对象就会从 A 中移除再加入到 B 中。类似于 dom 操作中的appendChild
方法的特性。向 Scene 中添加对象时可以给对象添加一个
name
对象,由此可以通过Scene.getObjectByName(name)
方法来获取对应的对象。 -
attach 方法与 add 方法效果几乎一样,不同之处是:依照上面的例子 a 对象添加到 B 中后会保持之前的平移旋转属性,而调用 add 则不会保留。
-
getObjectById 方法
getObjectById(id: number): Object3D | undefined
每一个继承自Object3D
的对象都有一个 id 并属具有唯一性,第一个对象 id 为 1,第二个对象的 id 为 2 … and so on。 -
getObjectByName
-
remove 方法
remove(...object: Object3D[]): this
删除传入的元素,传入的元素要是该对象的子元素。还可以通过操作 Scene.children属性来删除对象如:Scene.children.pop()
-
clear 方法 删除scene中的所有的子元素
属性
-
fog 烟雾效果 添加了该效果后,物体距离相机越远就越会被烟雾效果所遮挡,越近就越清晰。
-
fog 的创建方式 1
color
指定雾的颜色,near
指的是距离相机多远的距离开始产生雾的效果,far
是距相机多远雾效果消失。scene.fog = new THREE.Fog(color, near, far)
-
fog 的创建方式 2
density
指的是雾的密度增长速度 默认值是 0.00025scene.fog = new THREE.FogExp2(color, density)
-
-
background 用来设置场景的背景效果
background: null | Color | Texture
可以设置为纯色和纹理对象(Textrue),纹理又分两种:普通的图片和环境贴图(environment map, 是360度的拼接图)。
scene.background = new THREE.Color(0x44ff44);
const textureLoader = new THREE.TextureLoader()
// 普通图片形式
textureLoader.load('/bg.png', (loaded) => {
scene.background = loaded;
})
// 环境贴图形式
textureLoader.load('/env_map.png', (loaded) => {
loaded.mapping = THREE.EquirectangularReflectionMapping;
scene.background = loaded
})
加载纹理或立方体贴图是异步完成的,我们必须等待 THREE.TextureLoader
加载图像数据,然后才能将其赋值给背景。在加载环境贴图时需要告诉 three.js 我们加载的是环境贴图所以有 loaded.mapping = THREE.EquirectangularReflectionMapping;
怎么一句,之后讲纹理的文章再深入讨论。
- overrideMaterial 用来覆盖设置场景中所有 Mesh 的材质。一旦设置后scene中所有的 Mesh 材质都会变成所设置的值。
scene.overrideMaterial = new THREE.MeshNormalMaterial()
除了使用上面的操作,Three.js 还提供了一种设置环境贴图的方法将每个 Mesh 的材质的属性覆盖为相同的值。
textureLoader.load("/assets/equi.jpeg", (loaded) => {
loaded.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = loaded; // 设置环境贴图
});
这样设置后你可以看到下面 demo 中,在点击 “Toggle environment” 后所有的立方体表面不再是纯色,而是反映了环境贴图的一部分。
完整代码示例
import WebGLContainer from "@utils/webGLContainer"
import { useRef, useEffect } from "react"
import * as THREE from 'three'
import Stats from 'three/examples/jsm/libs/stats.module'
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"
import GUI from "lil-gui"
import { randomColor } from "@utils/colorUtil"
import { randomVector } from "@utils/positionUtil"
import resizeRendererToDisplaySize from "@utils/resizeRendererToDisplaySize"
import { FogExp2 } from "three"
const useDemo = () => {
const ContainerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const container = ContainerRef.current!
// 场景
const scene = new THREE.Scene()
scene.background = new THREE.Color(0xffffff)
// 雾效果
scene.fog = new THREE.Fog(0xffffff, 0.0025, 20)
// scene.fog = new THREE.FogExp2(0xffffff, 0.05)
// 初始化相机
const aspect = container.clientWidth / container.clientHeight
const camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000)
camera.position.set(-7, 2, 5)
// 初始化 renderer
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.outputEncoding = THREE.sRGBEncoding
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.VSMShadowMap
renderer.setClearColor(0xffffff)
// 把 canvas 加入到 dom 中
container.appendChild(renderer.domElement)
// 环境光
scene.add(new THREE.AmbientLight(0x666666))
// 平行光
const dirLight = new THREE.DirectionalLight(0xaaaaaa)
dirLight.position.set(5, 12, 8)
dirLight.castShadow = true
dirLight.intensity = 1
dirLight.shadow.camera.near = 0.1
dirLight.shadow.camera.far = 200
dirLight.shadow.camera.right = 10
dirLight.shadow.camera.left = -10
dirLight.shadow.camera.top = 10
dirLight.shadow.camera.bottom = -10
dirLight.shadow.mapSize.width = 2048
dirLight.shadow.mapSize.height = 2048
dirLight.shadow.radius = 4
dirLight.shadow.bias = -0.00005
scene.add(dirLight)
// 地板 floor 几何体
const geo = new THREE.BoxGeometry(10, 0.25, 10, 10, 10, 10)
const mat = new THREE.MeshStandardMaterial({ color: 0xdddddd })
const mesh = new THREE.Mesh(geo, mat)
mesh.position.set(0, -2, -1)
mesh.receiveShadow = true
mesh.name = 'floating-floor'
scene.add(mesh)
// 轨道控制器
const orbitControl = new OrbitControls(camera, renderer.domElement)
orbitControl.enableDamping = true
orbitControl.dampingFactor = 0.15
orbitControl.minDistance = 1
orbitControl.maxDistance = 100
orbitControl.minPolarAngle = Math.PI / 4
orbitControl.maxPolarAngle = (3 / 4) * Math.PI
orbitControl.update()
// 性能就监控器
const stats = Stats()
stats.domElement.classList.add('stats-bar')
container.appendChild(stats.domElement)
// 创建控制器可视化面板
const gui = new GUI({ autoPlace: false })
container.appendChild(gui.domElement)
// 坐标轴 helper
const axisHelperName = 'axesHelper'
const gridHelperName = 'gridHelper'
const polarGridHelperName = 'polarGridHelper'
const helperProps = {
axisHelper: {
toggle: () => {
const curHelper = scene.getObjectByName(axisHelperName)
if (curHelper) {
scene.remove(curHelper)
} else {
const helper = new THREE.AxesHelper(20)
helper.name = axisHelperName
scene.add(helper)
}
}
},
gridHelper: {
toggle: () => {
const curHelper = scene.getObjectByName(gridHelperName)
if (curHelper) {
scene.remove(curHelper)
} else {
const helper = new THREE.GridHelper(20, 20)
helper.name = gridHelperName
scene.add(helper)
}
}
},
polarGridHelper: {
toggle: () => {
const curHelper = scene.getObjectByName(polarGridHelperName)
if (curHelper) {
scene.remove(curHelper)
} else {
const helper = new THREE.PolarGridHelper(20, 16, 8, 128)
helper.name = polarGridHelperName
scene.add(helper)
}
}
},
}
const helperControl = gui.addFolder('Helper')
helperControl.add(helperProps.axisHelper, 'toggle').name('Toggle AxisHelper')
helperControl.add(helperProps.gridHelper, 'toggle').name('Toggle GridHelper')
helperControl.add(helperProps.polarGridHelper, 'toggle').name('Toggle PolarGridHelper')
// 场景的控制 control
const textureLoader = new THREE.TextureLoader()
const sceneControlProps = {
backGround: 'white',
enviroment: {
toggle: () => {
if (scene.environment) {
scene.environment = null
} else {
textureLoader.load('/assets/equi.jpeg', (loaded) => {
loaded.mapping = THREE.EquirectangularReflectionMapping
scene.environment = loaded
})
}
}
},
material: {
toggle: () => {
if (scene.overrideMaterial) {
scene.overrideMaterial = null
} else {
scene.overrideMaterial = new THREE.MeshNormalMaterial()
}
}
}
}
const sceneControl = gui.addFolder('Scene')
const backgrounds = ['white', 'black', 'green', 'texture', 'cubMap']
type backgroundType = 'white' | 'black' | 'green' | 'texture' | 'cubMap'
sceneControl.add(sceneControlProps.enviroment, 'toggle').name('Toggle enviroment')
sceneControl.add(sceneControlProps.material, 'toggle').name('Toggle material')
sceneControl.add(sceneControl, 'backGround', backgrounds).onChange((event: backgroundType) => {
switch (event) {
case 'black':
case 'green':
case 'white':
scene.background = new THREE.Color(event)
break;
case 'texture':
textureLoader.load('/assets/bg.jpg', (loaded) => {
loaded.mapping = THREE.EquirectangularReflectionMapping
scene.background = loaded
scene.environment = null
})
break;
case 'cubMap':
textureLoader.load('/assets/equi.jpeg', (loaded) => {
loaded.mapping = THREE.EquirectangularReflectionMapping
scene.background = loaded
scene.environment = loaded
})
break;
default:
break;
}
})
// 添加立方体
const addCube = () => {
const color = randomColor()
const pos = randomVector({
xRange: { fromX: -4, toX: 4 },
yRange: { fromY: -3, toY: 3 },
zRange: { fromZ: -4, toZ: 4 }
})
const rotation = randomVector({
xRange: { fromX: 0, toX: Math.PI * 2 },
yRange: { fromY: 0, toY: Math.PI * 2 },
zRange: { fromZ: 0, toZ: Math.PI * 2 }
})
const cubeGemotry = new THREE.BoxGeometry(1, 1, 1)
const cubeMate = new THREE.MeshStandardMaterial({
color,
roughness: 0.1, // 粗糙性
metalness: 0.9 // 金属性贴图
})
const cube = new THREE.Mesh(cubeGemotry, cubeMate)
cube.name = 'cube-' + scene.children.length
cube.position.copy(pos)
cube.rotation.setFromVector3(rotation)
cube.castShadow = true
scene.add(cube)
}
// 删除立方体
const removeCubde = () => {
scene.children.pop()
}
const cubeOptsPorps = {
addCube,
removeCubde
}
gui.add(cubeOptsPorps, 'addCube')
gui.add(cubeOptsPorps, 'removeCubde')
const resizer = () => {
const canvas = renderer.domElement
camera.aspect = canvas.clientWidth / canvas.clientHeight // 计算canvas的宽高比
camera.updateProjectionMatrix() //更新投影矩阵
}
window.addEventListener('resize', resizer)
function animate() {
renderer.render(scene, camera)
stats.update()
orbitControl.update()
requestAnimationFrame(animate)
}
resizeRendererToDisplaySize(renderer)
animate()
})
return <WebGLContainer ref={ContainerRef} fullScreen={true} />
}
export default useDemo
Geometry
我们在之前的示例中都看到过 Geometry 的创建和使用。Geometry 用于存放几何体的顶点信息,它塑造了被创造的几何体的形状。
几何体的创建
我们可以通过实例化 three.js 所提供的几何体类来创建几何体,前面示例都是用的此方式,还可以通过给定顶点信息来创建几何体。
通过给定顶点的方式创建一个立方体。
首先给出立方体8个顶点的坐标:
const v = [
[1, 3, 1],
[1, 3, -1],
[1, -1, 1],
[1, -1, -1],
[-1, 3, -1],
[-1, 3, 1],
[-1, -1, -1],
[-1, -1, 1]
]
然后用这 8 个顶点来组成面:
每个面都是由一个或多个三角形拼接组成的,立方体的面是长方形,可用两个三角形组成。在 Three.js 的早期版本中,可以使用四边形代替三角形。一个四边形使用四个顶点而不是三个顶点来定义面。使用四边形还是三角形更好在3D建模界掀起了激烈的争论。基本上,使用四边形通常是首选的,因为它们比三角形更容易增强和平滑。不过在用于渲染和游戏引擎中,使用三角形通常更容易,因为每个形状都可以非常有效地使用三角形渲染出来。
必须注意用于定义面的顶点顺序。它们的定义顺序决定了 Three.js 认为它是正面(面向相机的脸)还是背面。如果创建正面,应使用顺时针顺序创建正面,如果要创建背面,则应使用逆时针顺序。
// 创建几何体面数据
const faces = new Float32Array([
...v[1], ...v[0], ...v[2], // 一个三角面
...v[2], ...v[3], ...v[1], // 此三角面和上面的组成立方体的一个面
...v[4], ...v[6], ...v[5],
...v[6], ...v[7], ...v[5],
...v[4], ...v[5], ...v[1],
...v[5], ...v[0], ...v[1],
...v[7], ...v[6], ...v[2],
...v[6], ...v[3], ...v[2],
...v[5], ...v[7], ...v[0],
...v[7], ...v[2], ...v[0],
...v[1], ...v[3], ...v[4],
...v[3], ...v[6], ...v[4]
])
使用特定顺序的顶点信息创建几何体:
const bufferGemotry = new BufferGeometry()
// 创建几何体
bufferGemotry.setAttribute('position', new THREE.BufferAttribute(faces, 3))
bufferGemotry.computeVertexNormals() // 计算法向量
const mesh = new THREE.Mesh(bufferGemotry, new THREE.MeshNormalMaterial())
scene.add(mesh)
BufferAttribute 的第二个参数 itemSize :应与特定顶点关联的数组值的数量。例如,如果该属性存储的是三分量向量,则 itemSize 应为 3。
computeVertexNormals
当我们调用此函数时,Three.js 确定每个面的法向量。这是 Three.js 用于确定如何基于场景中的各种灯光为物体表面着色的信息。
默认情况下由于性能的原因 three.js 假设 mesh 的 geometry 不会发生改变。如果要改变 geometry 就需要告诉 three.js,可以通过设置 needsUpdate 属性来告知。
mesh.geometry.attributes.position.needsUpdate = true;
mesh.geometry.computeVertexNormals();
几何体克隆
使用 BufferGeometry.clone
方法进行克隆,克隆返回一个新的几何体对象。
const clonedGemo = bufferGemotry.clone()
// 获取克隆后几何体的顶点坐标数组
const positionArr = clonedGemo.getAttribute('position').array as Array<number>
// 把 z 坐标全部+3,沿着 z 轴正方向平移 3 个单位
for (let i = 0; i < positionArr.length; i++) {
if ((i + 1) % 3 === 0) {
positionArr[i] = positionArr[i] + 3
}
}
// 材质数组
const materials = [
new THREE.MeshBasicMaterial({
color: 'green',
wireframe: true
}),
new THREE.MeshLambertMaterial({
opacity: 0.1,
color: 'green',
transparent: true,
}),
];
// 给几何体使用多种材料
const mesh = createMultiMaterialObject(clonedGemo, materials);
scene.add(mesh)
three.js 提功力一个 createMultiMaterialObject
方法允许给一个几何体使用多个材料。注意此方法返回的是一个 THREE.Group
,它包含我们提供的每一种材料而生成的 mesh。 如果想改变 Mesh 的属性就需要遍历所有生成的 mesh 并修改属性。
mesh.children.forEach(function (e) {
e.castShadow = true;
});
完整示例:
import WebGLContainer from "@utils/webGLContainer"
import { useRef, useEffect } from 'react'
import * as THREE from 'three'
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"
import resizeRendererToDisplaySize from "@utils/resizeRendererToDisplaySize"
import Stats from 'three/examples/jsm/libs/stats.module'
import { BufferGeometry } from "three"
import GUI from "lil-gui"
import { createMultiMaterialObject } from "three/examples/jsm/utils/SceneUtils"
const v = [
[1, 3, 1],
[1, 3, -1],
[1, -1, 1],
[1, -1, -1],
[-1, 3, -1],
[-1, 3, 1],
[-1, -1, -1],
[-1, -1, 1]
]
let bufferGemotry: THREE.BufferGeometry
function updateCustomGeometry(scene: THREE.Scene) {
const p = scene.getObjectByName('customGeometry')
if (p) {
return
}
// 创建几何体
const faces = new Float32Array([
...v[1], ...v[0], ...v[2],
...v[2], ...v[3], ...v[1],
...v[4], ...v[6], ...v[5],
...v[6], ...v[7], ...v[5],
...v[4], ...v[5], ...v[1],
...v[5], ...v[0], ...v[1],
...v[7], ...v[6], ...v[2],
...v[6], ...v[3], ...v[2],
...v[5], ...v[7], ...v[0],
...v[7], ...v[2], ...v[0],
...v[1], ...v[3], ...v[4],
...v[3], ...v[6], ...v[4]
])
bufferGemotry = new BufferGeometry()
bufferGemotry.setAttribute('position', new THREE.BufferAttribute(faces, 3))
bufferGemotry.computeVertexNormals() // 计算法向量
const mesh = meshFromGeometry(bufferGemotry)
mesh.name = 'customGeometry'
scene.add(mesh)
}
function meshFromGeometry(geometry: THREE.BufferGeometry) {
const materials = [
new THREE.MeshBasicMaterial({
color: 0xff0000,
wireframe: true
}),
new THREE.MeshLambertMaterial({
opacity: 0.1,
color: 0xff0044,
transparent: true,
}),
]
const mesh = createMultiMaterialObject(geometry, materials)
mesh.children.forEach(child => {
child.castShadow = true
})
return mesh
}
function meshFromGeometry1(geometry: THREE.BufferGeometry) {
const wireframe = new THREE.WireframeGeometry(geometry)
const line = new THREE.LineSegments(wireframe, new THREE.MeshBasicMaterial({ color: 'red' }))
line.castShadow = true
return line
}
// clone 几何体
const cloneGemotrey = (scene: THREE.Scene) => {
const p = scene.getObjectByName("cloned gemotrey");
if (p) {
scene.remove(p);
return
}
const clonedGeo = bufferGemotry.clone()
const positionArr = clonedGeo.getAttribute('position').array as Array<number>
for (let i = 0; i < positionArr.length; i++) {
if ((i + 1) % 3 === 0) {
positionArr[i] = positionArr[i] + 3
}
}
clonedGeo.getAttribute('position').needsUpdate = true
const cloned = meshFromGeometry(clonedGeo)
cloned.children.forEach(child => {
child.castShadow = true
})
cloned.name = 'cloned gemotrey'
scene.add(cloned)
}
const customGeometry = () => {
const ContainerRef = useRef<HTMLDivElement>(null)
const isAni = useRef<boolean>(true)
useEffect(() => {
const container = ContainerRef.current!
const scene = new THREE.Scene()
scene.fog = new THREE.Fog(0xffffff, 20, 100)
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 100)
camera.position.set(-7, 2, 5)
camera.lookAt(0, 0, 0)
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.outputEncoding = THREE.sRGBEncoding
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.VSMShadowMap
renderer.setClearColor(0xffffff)
container.appendChild(renderer.domElement)
scene.add(new THREE.AmbientLight(0x666666))
const dirLight = new THREE.DirectionalLight(0xaaaaaa)
dirLight.position.set(5, 12, 8)
dirLight.castShadow = true
dirLight.intensity = 1
dirLight.shadow.camera.near = 0.1
dirLight.shadow.camera.far = 200
dirLight.shadow.camera.right = 10
dirLight.shadow.camera.left = -10
dirLight.shadow.camera.top = 10
dirLight.shadow.camera.bottom = -10
dirLight.shadow.mapSize.width = 2048
dirLight.shadow.mapSize.height = 2048
dirLight.shadow.radius = 4
dirLight.shadow.bias = -0.00005
scene.add(dirLight)
const geo = new THREE.BoxGeometry(15, 0.25, 15, 10, 10, 10)
const mat = new THREE.MeshStandardMaterial({ color: 0xdddddd })
const floor = new THREE.Mesh(geo, mat)
floor.position.set(0, -2, 0)
floor.receiveShadow = true
floor.name = 'floating-floor'
scene.add(floor)
const orbitContorl = new OrbitControls(camera, renderer.domElement)
orbitContorl.enableDamping = true
orbitContorl.dampingFactor = 0.15
orbitContorl.maxPolarAngle = Math.PI * 2
orbitContorl.minPolarAngle = 0
orbitContorl.maxDistance = 100
orbitContorl.minDistance = 0.1
const gui = new GUI({ autoPlace: false })
container.appendChild(gui.domElement)
// Helper
const axesHelperName = 'axesHelper'
const gridHelperName = 'gridHelper'
const polarHelperName = 'polarHelper'
const helper = {
axesHelper: {
toggle() {
const helper = scene.getObjectByName(axesHelperName)
if (helper) {
scene.remove(helper)
} else {
const helper = new THREE.AxesHelper(10)
helper.name = axesHelperName
scene.add(helper)
}
}
},
gridHelper: {
toggle() {
const helper = scene.getObjectByName(gridHelperName)
if (helper) {
scene.remove(helper)
} else {
const helper = new THREE.GridHelper(10, 10)
helper.name = gridHelperName
scene.add(helper)
}
}
},
polarHelper: {
toggle() {
const helper = scene.getObjectByName(polarHelperName)
if (helper) {
scene.remove(helper)
} else {
const helper = new THREE.PolarGridHelper(5, 16, 8)
helper.name = polarHelperName
scene.add(helper)
}
}
}
}
const sceneFloder = gui.addFolder('Scene')
sceneFloder.add(helper.axesHelper, 'toggle').name("Toggle AxesHelper")
sceneFloder.add(helper.gridHelper, 'toggle').name("Toggle GridHelper")
sceneFloder.add(helper.polarHelper, 'toggle').name("Toggle PolarHelper")
// Vextex 改变
const map = ['x', 'y', 'z']
v.forEach((vItem, idx) => {
const floder = gui.addFolder(`vertex${idx}`)
const property = { x: vItem[0], y: vItem[1], z: vItem[2] }
for (let i = 0; i < 3; i++) {
floder.add(property, map[i], vItem[i] - 1, vItem[i] + 1).onChange((event: number) => {
const positionAttr = bufferGemotry.getAttribute('position')
v[idx][i] = event
positionAttr.needsUpdate = true
const p = scene.getObjectByName('customGeometry')
if (p) scene.remove(p)
updateCustomGeometry(scene)
})
}
floder.close()
})
gui.add({ cloneGemotrey: () => cloneGemotrey(scene) }, 'cloneGemotrey').name('clone')
const stats = Stats()
stats.domElement.classList.add('stats-bar')
container.appendChild(stats.domElement)
const resizer = () => {
const canvas = renderer.domElement
camera.aspect = canvas.clientWidth / canvas.clientHeight // 计算canvas的宽高比
camera.updateProjectionMatrix() //更新投影矩阵
}
window.addEventListener('resize', resizer)
function animation() {
orbitContorl.update()
stats.update()
updateCustomGeometry(scene)
renderer.render(scene, camera)
if (isAni.current) {
requestAnimationFrame(animation)
}
}
resizeRendererToDisplaySize(renderer)
animation()
return () => {
window.removeEventListener('resize', resizer)
isAni.current = false
}
}, [])
return <WebGLContainer ref={ContainerRef} fullScreen={true} />
}
export default customGeometry
Mesh
我们创建一个 Geometry 用于塑造几何体的形状、一种 material(材质)来覆盖这个几何体表面,这两部分构成了能够加入场景中的3D对象也就是 Mesh。
Mesh对象的属性和方法:
- position 该属性确定相对于父容器的位置,父容器可能是
THREE.Scene
或者THREE.Group
。
设置方式:
mesh.position.x = 10
mesh.position.y = 10
mesh.position.z = 10
或者
mesh.position.set(x, y, z)
mesh.position = new THREE.Vector3(10,0,0);
- rotation 该属性表示 mesh 以 x、y、z 轴为旋转轴,所旋转的角度位置。
设置方式:
mesh.rotation.x = Math.PI
mesh.rotation.y = Math.PI
mesh.rotation.z = Math.PI
或者
mesh.rotation.set(0.5*Math.PI, 0, 0);
- scale 该属性表示 mesh 的放大缩小值。
mesh.scale.x = 1.5
mesh.scale.set(1, 1, 1.5)
- translateX/translateY/translateZ 该属性表示 mesh 距离原来位置的 x、y、z 轴方向上的偏移量。
mesh.translateX(10)
- lookAt 此属性将对象指向空间中的特定向量。这是手动设置旋转的替代方法。mesh 的 Z 轴正方向朝向设置的向量方向。
mesh.lookAt(0,0,1)
mesh.lookAt(new THREE.Vector3(1,1,1))
- visible 设置 mesh 对象是否会被渲染。
mesh.visible = true // falses
- castShadow 设置 mesh 在光照下能否产生阴影。
mesh.castShadow = true // false
完整示例:
import WebGLContainer from "@utils/webGLContainer"
import { useRef, useEffect } from 'react'
import * as THREE from 'three'
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"
import resizeRendererToDisplaySize from "@utils/resizeRendererToDisplaySize"
import Stats from 'three/examples/jsm/libs/stats.module'
import GUI from "lil-gui"
const customGeometry = () => {
const ContainerRef = useRef<HTMLDivElement>(null)
const isAni = useRef<boolean>(true)
useEffect(() => {
const container = ContainerRef.current!
const scene = new THREE.Scene()
scene.fog = new THREE.Fog(0xffffff, 20, 100)
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 100)
camera.position.set(-7, 2, 5)
camera.lookAt(0, 0, 0)
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.outputEncoding = THREE.sRGBEncoding
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.VSMShadowMap
renderer.setClearColor(0xffffff)
container.appendChild(renderer.domElement)
scene.add(new THREE.AmbientLight(0x666666))
const dirLight = new THREE.DirectionalLight(0xaaaaaa)
dirLight.position.set(5, 12, 8)
dirLight.castShadow = true
dirLight.intensity = 1
dirLight.shadow.camera.near = 0.1
dirLight.shadow.camera.far = 200
dirLight.shadow.camera.right = 10
dirLight.shadow.camera.left = -10
dirLight.shadow.camera.top = 10
dirLight.shadow.camera.bottom = -10
dirLight.shadow.mapSize.width = 2048
dirLight.shadow.mapSize.height = 2048
dirLight.shadow.radius = 4
dirLight.shadow.bias = -0.00005
scene.add(dirLight)
const geo = new THREE.BoxGeometry(10, 0.25, 10, 10, 10, 10)
const mat = new THREE.MeshStandardMaterial({ color: 0xdddddd })
const floor = new THREE.Mesh(geo, mat)
floor.position.set(0, -2, 0)
floor.receiveShadow = true
floor.name = 'floating-floor'
scene.add(floor)
const geom = new THREE.BoxGeometry(2, 2, 2, 10, 10, 10)
const material = new THREE.MeshStandardMaterial({ color: '#08f6bb' })
const mesh = new THREE.Mesh(geom, material)
mesh.castShadow = true
scene.add(mesh)
const gui = new GUI({ autoPlace: false })
container.appendChild(gui.domElement)
// Helper
const axesHelperName = 'axesHelper'
const gridHelperName = 'gridHelper'
const polarHelperName = 'polarHelper'
const helper = {
axesHelper: {
toggle() {
const helper = scene.getObjectByName(axesHelperName)
if (helper) {
scene.remove(helper)
} else {
const helper = new THREE.AxesHelper(10)
helper.name = axesHelperName
scene.add(helper)
}
}
},
gridHelper: {
toggle() {
const helper = scene.getObjectByName(gridHelperName)
if (helper) {
scene.remove(helper)
} else {
const helper = new THREE.GridHelper(10, 10)
helper.name = gridHelperName
scene.add(helper)
}
}
},
polarHelper: {
toggle() {
const helper = scene.getObjectByName(polarHelperName)
if (helper) {
scene.remove(helper)
} else {
const helper = new THREE.PolarGridHelper(5, 16, 8)
helper.name = polarHelperName
scene.add(helper)
}
}
}
}
const sceneFloder = gui.addFolder('Scene')
sceneFloder.add(helper.axesHelper, 'toggle').name("Toggle AxesHelper")
sceneFloder.add(helper.gridHelper, 'toggle').name("Toggle GridHelper")
sceneFloder.add(helper.polarHelper, 'toggle').name("Toggle PolarHelper")
const props = {
position: {
x: 0, y: 0, z: 0
},
translate: {
x: 0, y: 0, z: 0
},
scale: {
x: 1, y: 1, z: 1
},
rotation: {
x: 0, y: 0, z: 0
},
lookAt: {
x: 0, y: 0, z: 0
},
visible() {
mesh.visible = !mesh.visible
},
castShadow() {
mesh.castShadow = !mesh.castShadow
},
rotateOnWorldAxisX() {
mesh.rotateX(Math.PI / 4)
},
rotateOnWorldAxisY() {
mesh.rotateY(Math.PI / 4)
},
rotateOnWorldAxisZ() {
mesh.rotateZ(Math.PI / 4)
}
}
// visible
gui.add(props, 'visible').name('Visible')
// castShadow
gui.add(props, 'castShadow').name('CastShadow')
// rotateOnWorldAxisX
gui.add(props, 'rotateOnWorldAxisX').name('rotateOnWorldAxisX')
// rotateOnWorldAxisY
gui.add(props, 'rotateOnWorldAxisY').name('rotateOnWorldAxisY')
// rotateOnWorldAxisZ
gui.add(props, 'rotateOnWorldAxisZ').name('rotateOnWorldAxisZ')
// position
const positionControl = gui.addFolder('Position')
Object.keys(props.position).forEach((key) => {
positionControl.add(props.position, key, -5, 5).onChange(() => {
mesh.position.set(props.position.x, props.position.y, props.position.z)
// mesh.position.x == ??
// mesh.position.y == ??
// mesh.position.z == ??
})
})
// scale
const scaleControl = gui.addFolder('Scale')
Object.keys(props.scale).forEach((key) => {
scaleControl.add(props.scale, key, -5, 5).onChange(() => {
mesh.scale.set(props.scale.x, props.scale.y, props.scale.z)
})
})
// rotation
const rotationControl = gui.addFolder('Rotation')
Object.keys(props.rotation).forEach((key) => {
rotationControl.add(props.rotation, key, 0, Math.PI).onChange(() => {
mesh.rotation.set(props.rotation.x, props.rotation.y, props.rotation.z)
})
})
// lookAt
const lookAtControl = gui.addFolder('LookAt')
Object.keys(props.lookAt).forEach((key) => {
lookAtControl.add(props.lookAt, key, -5, 5)
})
lookAtControl.add({
lookAt() {
mesh.lookAt(props.lookAt.x, props.lookAt.y, props.lookAt.z)
}
}, 'lookAt')
// translate
const translateControl = gui.addFolder('Translate')
Object.keys(props.translate).forEach((key) => {
translateControl.add(props.translate, key, -5, 5)
})
translateControl.add({
translate() {
mesh.translateX(props.translate.x)
mesh.translateY(props.translate.y)
mesh.translateZ(props.translate.z)
}
}, 'translate')
const orbitContorl = new OrbitControls(camera, renderer.domElement)
orbitContorl.enableDamping = true
orbitContorl.dampingFactor = 0.15
orbitContorl.maxPolarAngle = Math.PI * 2
orbitContorl.minPolarAngle = 0
orbitContorl.maxDistance = 100
orbitContorl.minDistance = 0.1
const stats = Stats()
stats.domElement.classList.add('stats-bar')
container.appendChild(stats.domElement)
const resizer = () => {
const canvas = renderer.domElement
camera.aspect = canvas.clientWidth / canvas.clientHeight // 计算canvas的宽高比
camera.updateProjectionMatrix() //更新投影矩阵
}
window.addEventListener('resize', resizer)
function animation() {
orbitContorl.update()
stats.update()
renderer.render(scene, camera)
if (isAni.current) {
requestAnimationFrame(animation)
}
}
resizeRendererToDisplaySize(renderer)
animation()
return () => {
window.removeEventListener('resize', resizer)
isAni.current = false
}
}, [])
return <WebGLContainer ref={ContainerRef} fullScreen={true} />
}
export default customGeometry
Camera
PerspectiveCamera
透视相机,远小近大
properties
- fov:(Field of View 视野角度)通常,对于游戏,FOV 选择在 60 到 90 度之间。良好默认值:50;观察物体时,从物体两端(上、下、左、右)引出的光线在人眼光心处所成的夹角。 物体的尺寸越小,离观察者越远,则视角越小。 正常眼能区分物体上的两个点的最小视角约为1分。
- aspect: 渲染区域宽高比
- near: 该属性定义了相机到渲染场景的距离。通常,我们将其设置为一个非常小的值,以直接从摄影机的位置渲染所有内容。良好默认值:0.1
- far: 属性定义相机可以从相机的位置看到多远。如果设置得太低,可能无法渲染部分场景,如果设置得过高,在某些情况下,可能会影响渲染性能。良好默认值:100
- zoom: 缩放属性允许您放大和缩小场景。当使用小于1的数字时,将缩小场景,如果使用大于1的数字,则将放大。请注意,如果指定负值,则场景将呈现为倒置。良好默认值:1
相机的fov属性决定了水平fov。基于纵横特性(aspect),确定垂直FOV。近属性用于确定近平面的位置,远属性用于确定远平面的位置。近平面和远平面之间的区域将渲染如下:
OrthographicCamera
正交相机,所有的物体跟本身一样大。大小跟离摄像机的距离无关。通常运用在 2d 游戏里面
properties
正交投影对使用与纵横比或场景的FOV无关,因为所有对象都以相同的大小渲染。定义正交摄影机时,定义需要渲染的长方体区域。
- left: 这在Three.js文档中描述为相机截头体左平面。您应该将其视为将要渲染的内容的左侧边界。如果将该值设置为-100,则不会看到任何位置比左侧更远的对象。
- right :略
- top :略
- bottom :略
- near :略
- far :略
- zoom :略 渲染区域为一个长方体内部,通过这六个参数设置长方体的六个面的位置。
设置相机的看向的方向
默认情况下相机朝向(0,0,0)
camera.lookAt(new THREE.Vector3(x, y, z));
使用 lookAt
功能时,将相机指向特定位置。也可以使用该选项使摄影机跟随场景周围的对象。由于每个THREE.Mesh对象的位置都是THREE.Vector3对象,因此可以使用lookAt函数指向场景中的特定网格。你需要使用的就是这个:camera.lookAt(mesh.position)
。如果在渲染循环中调用这个,你将使摄影机跟随对象在场景中移动。
完整示例:
import resizeRendererToDisplaySize from "@utils/resizeRendererToDisplaySize"
import WebGLContainer from "@utils/webGLContainer"
import GUI from "lil-gui"
import { useRef, useEffect, ClassType } from 'react'
import { initializePerspectiveCameraControls, initializeOrthographicCameraControls } from "../../controls/camera-controls"
import * as THREE from 'three'
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"
import Stats from "three/examples/jsm/libs/stats.module"
const addCubes = (scene: THREE.Scene) => {
const size = 0.9
const cubeGeometry = new THREE.BoxGeometry(size, size, size)
for (var j = 0; j < 10; j++) {
for (var i = 0; i < 10; i++) {
var rnd = Math.random() * 0.75 + 0.25
var cubeMaterial = new THREE.MeshLambertMaterial()
cubeMaterial.color = new THREE.Color(rnd, 0, 0)
var cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
cube.position.z = -10 + 1 + j + 3.5
cube.position.x = -10 + 1 + i + 4.5
cube.position.y = -2
scene.add(cube)
}
}
}
const customGeometry = () => {
const ContainerRef = useRef<HTMLDivElement>(null)
const isAni = useRef<boolean>(true)
useEffect(() => {
const container = ContainerRef.current!
const scene = new THREE.Scene()
scene.fog = new THREE.Fog(0xffffff, 20, 100)
let camera: THREE.OrthographicCamera | THREE.PerspectiveCamera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 100)
camera.position.set(-7, 2, 5)
camera.lookAt(0, 0, 0)
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.outputEncoding = THREE.sRGBEncoding
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.VSMShadowMap
renderer.setClearColor(0xffffff)
container.appendChild(renderer.domElement)
scene.add(new THREE.AmbientLight(0x666666))
const dirLight = new THREE.DirectionalLight(0xaaaaaa)
dirLight.position.set(5, 12, 8)
dirLight.castShadow = true
dirLight.intensity = 1
dirLight.shadow.camera.near = 0.1
dirLight.shadow.camera.far = 200
dirLight.shadow.camera.right = 10
dirLight.shadow.camera.left = -10
dirLight.shadow.camera.top = 10
dirLight.shadow.camera.bottom = -10
dirLight.shadow.mapSize.width = 2048
dirLight.shadow.mapSize.height = 2048
dirLight.shadow.radius = 4
dirLight.shadow.bias = -0.00005
scene.add(dirLight)
const geo = new THREE.BoxGeometry(10, 0.25, 10, 10, 10, 10)
const mat = new THREE.MeshStandardMaterial({ color: 0xdddddd })
const floor = new THREE.Mesh(geo, mat)
floor.position.set(0, -2, -1)
floor.receiveShadow = true
floor.name = 'floating-floor'
scene.add(floor)
// 相机
addCubes(scene)
let orbitContorl = new OrbitControls(camera, renderer.domElement)
const gui = new GUI({ autoPlace: false })
container.appendChild(gui.domElement)
// Helper
const axesHelperName = 'axesHelper'
const gridHelperName = 'gridHelper'
const polarHelperName = 'polarHelper'
const helper = {
axesHelper: {
toggle() {
const helper = scene.getObjectByName(axesHelperName)
if (helper) {
scene.remove(helper)
} else {
const helper = new THREE.AxesHelper(10)
helper.name = axesHelperName
scene.add(helper)
}
}
},
gridHelper: {
toggle() {
const helper = scene.getObjectByName(gridHelperName)
if (helper) {
scene.remove(helper)
} else {
const helper = new THREE.GridHelper(10, 10)
helper.name = gridHelperName
scene.add(helper)
}
}
},
polarHelper: {
toggle() {
const helper = scene.getObjectByName(polarHelperName)
if (helper) {
scene.remove(helper)
} else {
const helper = new THREE.PolarGridHelper(5, 16, 8)
helper.name = polarHelperName
scene.add(helper)
}
}
}
}
const sceneFloder = gui.addFolder('Scene')
sceneFloder.add(helper.axesHelper, 'toggle').name("Toggle AxesHelper")
sceneFloder.add(helper.gridHelper, 'toggle').name("Toggle GridHelper")
sceneFloder.add(helper.polarHelper, 'toggle').name("Toggle PolarHelper")
const props = {
perspective: 'Perspective',
switchCamera: function () {
if (camera instanceof THREE.PerspectiveCamera) {
camera = new THREE.OrthographicCamera(
container.clientWidth / -8,
container.clientWidth / 8,
container.clientHeight / 8,
container.clientHeight / -8,
-10,
50
)
camera.position.set(-2, 2, 2)
camera.zoom = 12
camera.updateProjectionMatrix()
let orbitContorl = new OrbitControls(camera, renderer.domElement)
props.perspective = 'Orthographic'
initializeOrthographicCameraControls(camera, gui, orbitContorl)
orbitContorl.update()
} else {
camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000)
camera.position.set(-7, 2, 5)
camera.updateProjectionMatrix()
props.perspective = 'Perspective'
orbitContorl = new OrbitControls(camera, renderer.domElement)
initializePerspectiveCameraControls(camera, gui, orbitContorl)
orbitContorl.update()
}
}
}
gui.add(props, 'switchCamera')
gui.add(props, 'perspective').listen()
initializePerspectiveCameraControls(camera, gui, orbitContorl)
const stats = Stats()
stats.domElement.classList.add('stats-bar')
container.appendChild(stats.domElement)
const resizer = () => {
const canvas = renderer.domElement
if (camera instanceof THREE.PerspectiveCamera) {
camera.aspect = canvas.clientWidth / canvas.clientHeight // 计算canvas的宽高比
} else {
camera.left = container.clientWidth / -8
camera.right = container.clientWidth / 8
camera.top = container.clientHeight / 8
camera.bottom = container.clientHeight / -8
}
camera.updateProjectionMatrix() //更新投影矩阵
}
window.addEventListener('resize', resizer)
function animation() {
orbitContorl.update()
stats.update()
renderer.render(scene, camera)
if (isAni.current) {
requestAnimationFrame(animation)
}
}
resizeRendererToDisplaySize(renderer)
animation()
return () => {
window.removeEventListener('resize', resizer)
isAni.current = false
}
}, [])
return <WebGLContainer ref={ContainerRef} fullScreen={true} />
}
export default customGeometry
相机辅助工具
Three.js 提供了相机辅助工具,可以清晰明了的理解相机的位置和显示范围。
使用方式:
const cameraHelper = new THREE.CameraHelper(camera) // 传入需要辅助的相机
scene.add(cameraHelper) // 加入到 scene 中
完整示例:
示例中使用了两个摄像机,一个像机上添加了 helper,另一个相机用来观察 添加了 helper 的相机的状态。
import { useRef, useEffect, ClassType } from 'react'
import WebGLContainer from '@utils/webGLContainer'
import * as THREE from "three";
import { initScene, InitSceneOptiosns } from "../../bootstrap/bootstrap";
import { floatingFloor } from "../../bootstrap/floor";
import { initializeHelperControls } from "../../controls/helpers-control";
import GUI from "lil-gui";
import { initializePerspectiveCameraControls } from "../../controls/camera-controls";
import Stats from 'three/examples/jsm/libs/stats.module';
import resizeRendererToDisplaySize from '@utils/resizeRendererToDisplaySize';
const addCube = (scene: THREE.Scene) => {
const cubeGeom = new THREE.BoxGeometry(1, 1, 1);
const cubeMat = new THREE.MeshStandardMaterial({
color: 0x00ff00,
});
const mesh = new THREE.Mesh(cubeGeom, cubeMat);
mesh.castShadow = true;
scene.add(mesh);
};
const externalCamera = (container: HTMLElement) => {
const camera = new THREE.PerspectiveCamera(
75,
container.clientWidth / container.clientHeight,
0.1,
1000
);
// opposite of normally positioned camera
camera.position.set(10, 2, -3);
camera.lookAt(0, 0, 0);
return camera;
};
const DebugCamera = () => {
const ContainerRef = useRef<HTMLDivElement>(null)
const isAni = useRef<boolean>(true)
useEffect(() => {
const container = ContainerRef.current!
const gui = new GUI({ autoPlace: false });
container.appendChild(gui.domElement)
const stats = Stats()
stats.domElement.classList.add('stats-bar')
container.appendChild(stats.domElement)
const props: InitSceneOptiosns = {
backgroundColor: new THREE.Color('black'),
fogColor: 0xffffff,
container,
resize: false
};
let resizer: any
initScene(props)(({ scene, camera, renderer, orbitControls }) => {
camera.position.set(-7, 2, 5);
orbitControls.update();
const helper = new THREE.CameraHelper(camera);
scene.add(helper);
initializePerspectiveCameraControls(camera, gui, orbitControls);
floatingFloor(scene, 10);
const newCamera = externalCamera(container);
let renderWith = newCamera;
resizer = () => {
const canvas = renderer.domElement
renderWith.aspect = canvas.clientWidth / canvas.clientHeight // 计算canvas的宽高比
renderWith.updateProjectionMatrix() //更新投影矩阵
}
window.addEventListener('resize', resizer)
function animate() {
if (isAni.current) {
requestAnimationFrame(animate);
}
renderer.render(scene, renderWith);
stats.update();
helper.update();
orbitControls.update();
}
resizeRendererToDisplaySize(renderer)
animate();
gui.add(
{
switchCamera: () => {
if (renderWith == newCamera) {
renderWith = camera;
} else {
renderWith = newCamera;
}
resizer()
},
},
"switchCamera"
);
addCube(scene);
});
return () => {
window.removeEventListener('resize', resizer)
isAni.current = false
}
}, [])
return <WebGLContainer ref={ContainerRef} fullScreen={true} />
}
export default DebugCamera
【参考资料】
-
Learn three.js Fourth Edition
今天的文章[Three.js-04] Three.js 应用的基本组成分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/20320.html