下面将进一步详解介绍Threejs中的常用对象, 包括:
-
物体
-
-
几何
-
材质
-
纹理
-
-
场景
-
相机
-
光线与阴影(付费)
-
加载外部模型(付费)
-
粒子效果(付费)
-
动画与交互(付费)
-
物理仿真(付费)
本篇内容主要介绍物体基本原理(几何、材质和纹理)和几种几何体(立方体、平面、球体)的讲解。
其他的将在后续的更新中逐一介绍。
01 简介
如何在虚拟的3D场景中模拟真实世界, 核心是物体
在three.js
中, 物体使用Mesh
(网格)来描述
网格模型
在计算机图形学中, 几何体都是由三角形
拼接而成, 由三角形
组成的模型叫网格模型
从上面可以看到, 三角形越多, 模拟的模型越接近真实情况。
ChatGPT的解释
图形学中使用三角形作为基本的构造单元来生成复杂的物体,这是因为使用三角形可以带来多方面的好处。下面是一些主要的好处:
-
易于处理:三角形是一种简单的几何图形,易于计算和处理。它们有三个顶点,每个顶点有三个坐标,这使它们易于表示和存储。
-
适合多边形的形状:复杂的多边形形状可以通过简单的三角形组合来近似表示。这种近似往往能够满足需要,同时也可以提高计算效率。
-
易于进行光照和渲染:计算机图形学中的光照和渲染算法通常是针对三角形的。这是因为三角形是最简单的几何形状之一,它们的光照和渲染可以在多个工具和库中找到。
-
具有一致性:使用三角形作为构造单元可以带来一致性的好处。因为三角形是通用的基本几何形状,它们可以被用于不同的场景和应用中,从简单的2D图形到复杂的3D物体,这样可以使代码更加通用。
-
确保物体的平面:在三维空间中,三角形是唯一可以确保与另一个三角形在同一平面上的几何形状。这是因为只要三个点不共线,就可以构建出确定的平面,这保障了物体的正确表现。
综上,使用三角形作为构造单元有多种好处,使得其成为图形学中最受欢迎的几何形状之一。
Mesh
主要由这些部分组成
-
几何
-
材质
-
纹理
01
几何
如何理解几何?
几何描述了物体的形状
和大小
, 比如同样是桌子形状
, 大小
就各不相同
-
形状上: 有方形的, 圆形的
-
大小上: 有大的, 有小的
02
材质
如何理解材质
材质就是物体使用的材料, 材质不同对光线的反射效果不同
还是以桌子以例, 不同的桌面有的是木头
, 有的是大理石
, 有的是塑料(复合材料)
, 有的是金属
再比如, 门有木门
铁门
玻璃门
03
纹理
如何理解纹理?
纹理就是物体表面的图案花纹
还是以桌子为例, 不同木头, 表面的花纹不一样
02 几何详解
在threejs
中给我们内置了一些几何形状, 具体内容参见文档
在docs
中搜索geometry
几何体
这里, 我们主要介绍几个常用的几何体(立方体, 球体, 平面)
01
立方体
立方体, 我们主要需要设置其(长宽高)属性值
-
长(width): x轴所占据的空间
-
宽(depth): z轴所占据的空间
-
高(height): y轴所占据的空间
// 三. 创建物体
const cubeGeometry = new THREE.BoxGeometry(2, 2, 2)
const cubeMaterial = new THREE.MeshNormalMaterial()
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
scene.add(cube)
为了方便观察, 我们可以考虑使用Gui工具, 调整立方体的坐标和大小
完整示例:
// 导入threejs
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import * as dat from 'dat.gui'
// 一. 创建场景
const scene = new THREE.Scene()
// 二. 创建相机
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight
)
camera.position.set(20, 20, 20)
camera.lookAt(0, 0, 0)
// 三. 创建物体
const cubeGeometry = new THREE.BoxGeometry(2, 2, 2)
const cubeMaterial = new THREE.MeshNormalMaterial()
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
scene.add(cube)
// 六. 集成Gui工具
const gui = new dat.GUI()
const data = {
x: 0,
y: 0,
z: 0,
width: 2,
height: 2,
depth: 2,
}
gui.add(data, 'x').onChange((value) => {
cube.position.x = value
})
gui.add(data, 'y').onChange((value) => {
cube.position.y = value
})
gui.add(data, 'z').onChange((value) => {
cube.position.z = value
})
gui.add(data, 'width', 2, 20, 1).onChange((value) => {
data.width = value
// 销毁旧的几何体体
cube.geometry.dispose()
cube.geometry = new THREE.BoxGeometry(data.width, data.height, data.depth)
})
gui.add(data, 'height', 2, 20, 1).onChange((value) => {
data.height = value
// 销毁旧的几何体体
cube.geometry.dispose()
cube.geometry = new THREE.BoxGeometry(data.width, data.height, data.depth)
})
gui.add(data, 'depth', 2, 20, 1).onChange((value) => {
data.depth = value
// 销毁旧的几何体体
cube.geometry.dispose()
cube.geometry = new THREE.BoxGeometry(data.width, data.height, data.depth)
})
// 四. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(animation)
document.body.appendChild(renderer.domElement)
function animation() {
renderer.render(scene, camera)
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
// 五. 集成辅助工具
const control = new OrbitControls(camera, renderer.domElement)
const axesHepler = new THREE.AxesHelper(10)
scene.add(axesHepler)
const gridHelper = new THREE.GridHelper(20, 20, 0xffffff, 0xffffff)
gridHelper.material.transparent = true
gridHelper.material.opacity = 0.5
scene.add(gridHelper)
02
球体
球体, 我们主要修改其半径和分段
-
半径(radius): 球体半径
-
经线分段数(widthSegments):
-
纬线分段数(heightSegments):
const sphereGeometry = new THREE.SphereGeometry(1, 32, 32)
const sphereMaterial = new THREE.MeshNormalMaterial()
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
完整示例:
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import * as dat from 'dat.gui'
// 一. 创建场景
const scene = new THREE.Scene()
// 二. 创建相机
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight
)
camera.position.set(0, 0, 5)
// 三. 创建物体
const sphereGeometry = new THREE.SphereGeometry(1, 32, 32)
const sphereMaterial = new THREE.MeshNormalMaterial()
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
scene.add(sphere)
const data = {
x: 0,
y: 0,
z: 0,
radius: 1,
widthSegments: 32,
heightSegments: 32,
}
const gui = new dat.GUI()
gui.add(data, 'x').onChange((value) => {
sphere.position.x = value
})
gui.add(data, 'y').onChange((value) => {
sphere.position.y = value
})
gui.add(data, 'z').onChange((value) => {
sphere.position.z = value
})
gui.add(data, 'radius', 1, 10, 1).onChange((value) => {
data.radius = value
sphere.geometry.dispose()
sphere.geometry = new THREE.SphereGeometry(
data.radius,
data.widthSegments,
data.heightSegments
)
})
gui.add(data, 'widthSegments', 3, 64, 1).onChange((value) => {
data.widthSegments = value
sphere.geometry.dispose()
sphere.geometry = new THREE.SphereGeometry(
data.radius,
data.widthSegments,
data.heightSegments
)
})
gui.add(data, 'heightSegments', 2, 64, 1).onChange((value) => {
data.heightSegments = value
sphere.geometry.dispose()
sphere.geometry = new THREE.SphereGeometry(
data.radius,
data.widthSegments,
data.heightSegments
)
})
// 四. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(animation)
// 将渲染的canvas添加到body元素中
document.body.appendChild(renderer.domElement)
// 五. 辅助工具
const control = new OrbitControls(camera, renderer.domElement)
const axesHelper = new THREE.AxesHelper(10)
scene.add(axesHelper)
const gridHelper = new THREE.GridHelper(20, 20, 0xffffff, 0xffffff)
gridHelper.material.transparent = true
gridHelper.material.opacity = 0.5
scene.add(gridHelper)
function animation() {
renderer.render(scene, camera)
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
上面我们发现Gui部分的代码很多都是重复的。
考虑到后续其它对象也需要使用Gui工具, 我们可以考虑统一封装
创建src/gui
文件夹
在gui
文件夹下创建index.js
导出两个类
-
BaseGui
-
MeshGui
export { BaseGui } from './BaseGui'
export { MeshGui } from './MeshGui'
import * as dat from 'dat.gui'
export class BaseGui {
constructor() {
this.gui = new dat.GUI()
}
initGuiFolder(item, name, parent, config) {
const folder = this.gui.addFolder(name)
Object.keys(config).forEach((key) => {
this.initGuiItem(folder, item, key, parent, config[key])
})
}
initGuiItem(gui, item, key, parent, options = {}) {
// 构造数据
const controls = {}
if (key === 'color') {
controls[key] = item.color.getHex()
} else {
controls[key] = item[key]
}
// console.log(controls)
const method = options.method || 'add'
const min = options.min || (options.extend && options.extend[0])
const max = options.max || (options.extend && options.extend[1])
const step = options.step || 1
const name = options.name
let guiItem = gui[method](controls, key)
if (guiItem.min && guiItem.max && guiItem.step) {
guiItem = min !== undefined ? guiItem.min(min) : guiItem
guiItem = max !== undefined ? guiItem.max(max) : guiItem
guiItem = step !== undefined ? guiItem.step(step) : guiItem
}
guiItem = name !== undefined ? guiItem.name(name) : guiItem
guiItem.onChange((value) => {
if (options.handler) {
options.handler(item, key, value, parent)
} else {
item[key] = value
}
})
}
}
import * as THREE from 'three'
import { BaseGui } from './BaseGui'
function defaultHandler(item, key, value) {
item[key] = value
}
const Vector3Config = {
x: {
handler: defaultHandler,
},
y: {
handler: defaultHandler,
},
z: {
handler: defaultHandler,
},
}
function eulerHandler(item, key, value) {
item[key] = THREE.MathUtils.degToRad(value)
}
const EulerConfig = {
x: {
extend: [-180, 180],
handler: eulerHandler,
},
y: {
extend: [-180, 180],
handler: eulerHandler,
},
z: {
extend: [-180, 180],
handler: eulerHandler,
},
}
function geometryHandler(item, key, value, parent) {
const params = { ...parent.geometry.parameters }
params[key] = value
parent.geometry.dispose()
parent.geometry = new THREE[parent.geometry.type](...Object.values(params))
}
const GeometryMapping = {
BoxGeometry: {
width: {
name: 'x轴宽度',
extend: [2, 20],
handler: geometryHandler,
},
height: {
name: 'y轴高度',
extend: [2, 20],
handler: geometryHandler,
},
depth: {
name: 'z轴深度',
extend: [2, 20],
handler: geometryHandler,
},
},
SphereGeometry: {
radius: {
name: '半径',
min: 1,
handler: geometryHandler,
},
widthSegments: {
name: '水平分段数',
min: 3,
handler: geometryHandler,
},
heightSegments: {
name: '垂直分段数',
min: 2,
handler: geometryHandler,
},
},
PlaneGeometry: {
width: {
name: 'x轴宽度',
min: 1,
handler: geometryHandler,
},
height: {
name: 'y轴高度',
min: 1,
handler: geometryHandler,
},
},
}
export class MeshGui extends BaseGui {
constructor(options = {}) {
if (!options.target.isMesh) {
console.error('target must be an instance of Mesh')
return
}
super()
this.init(options)
}
init(options) {
this.mesh = options.target
this.geometry = this.mesh.geometry
this.material = this.mesh.material
this.position = this.mesh.position
this.rotation = this.mesh.rotation
this.scale = this.mesh.scale
options.position !== false ? this.initPosition() : ''
options.rotation !== false ? this.initRotation() : ''
options.scale !== false ? this.initScale() : ''
options.geometry !== false ? this.initGeometry() : ''
}
initPosition() {
console.log(this.position)
this.initGuiFolder(this.position, '位置', this.mesh, Vector3Config)
}
initRotation() {
this.initGuiFolder(this.rotation, '旋转(度)', this.mesh, EulerConfig)
}
initScale() {
this.initGuiFolder(this.scale, '缩放', this.mesh, Vector3Config)
}
initGeometry() {
const geometry = this.geometry
const type = geometry.type
const config = GeometryMapping[type]
this.initGuiFolder(geometry.parameters, geometry.type, this.mesh, config)
}
}
完整示例(优化封装版)
在球体几何中引用MeshGui
// 导入threejs
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
// 导入封装的Gui工具
import { MeshGui } from '../gui'
// 一. 创建场景
const scene = new THREE.Scene()
// 二. 创建相机
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight
)
camera.position.set(20, 20, 20)
camera.lookAt(0, 0, 0)
// 三. 创建物体
const sphereGeometry = new THREE.SphereGeometry(2)
const sphereMaterail = new THREE.MeshNormalMaterial() // 法向
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterail)
scene.add(sphere)
// 六. 集成Gui工具
new MeshGui({
target: sphere,
})
// 四. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(animation)
document.body.appendChild(renderer.domElement)
function animation() {
renderer.render(scene, camera)
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
// 五. 集成辅助工具
const control = new OrbitControls(camera, renderer.domElement)
const axesHepler = new THREE.AxesHelper(10)
scene.add(axesHepler)
const gridHelper = new THREE.GridHelper(20, 20, 0xffffff, 0xffffff)
gridHelper.material.transparent = true
gridHelper.material.opacity = 0.5
scene.add(gridHelper)
03
平面
对于平面, 主要设置
-
width: x轴方向的宽度
-
height: y轴方向的高度
const geometry = new THREE.PlaneGeometry(20, 20)
const material = new THREE.MeshBasicMaterial({ color: 0x333333 })
const plane = new THREE.Mesh(geometry, material)
完整示例:
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { MeshGui } from './gui'
// 一. 创建场景
const scene = new THREE.Scene()
// 二. 创建相机
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight
)
camera.position.set(0, 0, 5)
// 三. 创建物体
const geometry = new THREE.PlaneGeometry(20, 20)
const material = new THREE.MeshNormalMaterial()
const plane = new THREE.Mesh(geometry, material)
scene.add(plane)
new MeshGui({
target: plane,
})
// 四. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(animation)
// 将渲染的canvas添加到body元素中
document.body.appendChild(renderer.domElement)
// 五. 辅助工具
const control = new OrbitControls(camera, renderer.domElement)
const axesHelper = new THREE.AxesHelper(10)
scene.add(axesHelper)
const gridHelper = new THREE.GridHelper(20, 20, 0xffffff, 0xffffff)
gridHelper.material.transparent = true
gridHelper.material.opacity = 0.5
scene.add(gridHelper)
function animation() {
renderer.render(scene, camera)
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
04
缓冲区几何体
虽然three.js
给我们内置了很多常用的几何体
但是对于一些没有提供的几何体如何处理呢?
比如, 像下图所示的几何体
这里, 我们可以通过基类BufferGeometry
自定义几何体
缓冲区几何体
-
根据我们的几何常识: 连点成线, 连线成面, 连面成体
-
如果我们要渲染一个几何体, 只需要确定几何体的顶点坐标, 然后将这些点连接起来, 但是点太多, 为了方便管理我们使用一个缓冲区
Buffer
来存储, 因此自定义几何体
也称缓冲区几何体
步骤
-
实例化
BufferGeometry
对象 -
定义顶点坐标缓冲区(数组), 数组中每3个元素为一组, 表示一点的坐标
-
设置几何体的
position
属性
示例
// 1. 创建缓冲区几何体对象
const geometry = new THREE.BufferGeometry()
// 2. 定义顶点坐标缓冲区
const vertices = new Float32Array([
// 第一个三角形
-1.0, -1.0, 1.0, 1.0, -1.0, 0, 1.0, 1.0, 1.0,
// 第二个三角形
-1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 0,
])
// 3. 设置顶点坐标
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
const material = new THREE.MeshBasicMaterial({
color: 0xff0000,
wireframe: true,
})
const mesh = new THREE.Mesh(geometry, material)
完整示例:
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
// 一. 创建场景
const scene = new THREE.Scene()
// 二. 创建相机
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight
)
camera.position.set(0, 0, 5)
// 三. 创建物体
// 根据顶点构建三角形
// 1. 创建缓冲区几何体对象
const geometry = new THREE.BufferGeometry()
// 2. 定义顶点坐标缓冲区
const vertices = new Float32Array([
// 第一个三角形
-1.0, -1.0, 1.0, 1.0, -1.0, 0, 1.0, 1.0, 1.0,
// 第二个三角形
-1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 0,
])
// 3. 设置顶点坐标
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
const material = new THREE.MeshBasicMaterial({
color: 0xff0000,
// wireframe: true,
})
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
// 四. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(animation)
// 将渲染的canvas添加到body元素中
document.body.appendChild(renderer.domElement)
// 五. 辅助工具
const control = new OrbitControls(camera, renderer.domElement)
const axesHelper = new THREE.AxesHelper(10)
scene.add(axesHelper)
const gridHelper = new THREE.GridHelper(20, 20, 0xffffff, 0xffffff)
gridHelper.material.transparent = true
gridHelper.material.opacity = 0.5
scene.add(gridHelper)
function animation() {
renderer.render(scene, camera)
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})