《Three.JS零基础入门教程》-----之物体详解

下面将进一步详解介绍Threejs中的常用对象, 包括:

  • 物体

    • 几何

    • 材质

    • 纹理

  • 场景

  • 相机

  • 光线与阴影(付费)

  • 加载外部模型(付费)

  • 粒子效果(付费)

  • 动画与交互(付费)

  • 物理仿真(付费)

本篇内容主要介绍物体基本原理(几何、材质和纹理)和几种几何体(立方体、平面、球体)的讲解。

其他的将在后续的更新中逐一介绍。

01 简介

如何在虚拟的3D场景中模拟真实世界, 核心是物体

three.js中, 物体使用Mesh(网格)来描述

网格模型

在计算机图形学中, 几何体都是由三角形拼接而成, 由三角形组成的模型叫网格模型

图片

图片

从上面可以看到, 三角形越多, 模拟的模型越接近真实情况。

ChatGPT的解释

图形学中使用三角形作为基本的构造单元来生成复杂的物体,这是因为使用三角形可以带来多方面的好处。下面是一些主要的好处:

  1. 易于处理:三角形是一种简单的几何图形,易于计算和处理。它们有三个顶点,每个顶点有三个坐标,这使它们易于表示和存储。

  2. 适合多边形的形状:复杂的多边形形状可以通过简单的三角形组合来近似表示。这种近似往往能够满足需要,同时也可以提高计算效率。

  3. 易于进行光照和渲染:计算机图形学中的光照和渲染算法通常是针对三角形的。这是因为三角形是最简单的几何形状之一,它们的光照和渲染可以在多个工具和库中找到。

  4. 具有一致性:使用三角形作为构造单元可以带来一致性的好处。因为三角形是通用的基本几何形状,它们可以被用于不同的场景和应用中,从简单的2D图形到复杂的3D物体,这样可以使代码更加通用。

  5. 确保物体的平面:在三维空间中,三角形是唯一可以确保与另一个三角形在同一平面上的几何形状。这是因为只要三个点不共线,就可以构建出确定的平面,这保障了物体的正确表现。

综上,使用三角形作为构造单元有多种好处,使得其成为图形学中最受欢迎的几何形状之一。

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自定义几何体

缓冲区几何体

  1. 根据我们的几何常识: 连点成线, 连线成面, 连面成体

  2. 如果我们要渲染一个几何体, 只需要确定几何体的顶点坐标, 然后将这些点连接起来, 但是点太多, 为了方便管理我们使用一个缓冲区Buffer来存储, 因此自定义几何体也称缓冲区几何体

步骤

  1. 实例化BufferGeometry对象

  2. 定义顶点坐标缓冲区(数组), 数组中每3个元素为一组, 表示一点的坐标

  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)
})

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值