three.js 环境和语法

552 阅读6分钟

一、ES6方式导入three和扩展库

以r148为例,需要用到build和jsm目录下的js文件

image.png

index.js

import * as THREE from 'three' // 引入three
import { OrbitControls } from 'three/addons/controls/OrbitControls.js' // 引入three扩展库

console.log(THREE.Scene)
console.log(OrbitControls)

index.html

  <script type="importmap">
    {
      "imports": {
        "three":"./build/three.module.js",
        "three/addons/": "./jsm/"
      }
    }
  </script>

  <script type="module" src="./01-引入three.js">

  </script>

控制台可以打印出来对应的class,引入就没有问题

image.png

二、一个3D案例需要3个基本概念:场景、相机、渲染器

index.js

import * as THREE from 'three' // 引入three

// 创建一个三维场景scene
const scene = new THREE.Scene()

/*
  创建网络模型
*/
// 创建一个几何体 长方体 长宽高都是100
const geometry = new THREE.BoxGeometry(100, 100, 100)
// 创建一个材质对象
const material = new THREE.MeshBasicMaterial({ color: 0x0000ff })
// 创建网络模型对象
const mesh = new THREE.Mesh(geometry, material)
// 设置网络模型在三维空间中的位置坐标,默认是坐标原点
mesh.position.set(0, 10, 0)
// 网络模型添加到场景中
scene.add(mesh)

/*
  透视投影相机设置
*/
// 宽和高用来设置渲染后,输出的画布宽高
const width = 800
const height = 500
// 透视投影相机设置 30:视场角度 width / height:canvas画布宽高比 1:近裁截面 3000:远裁截面
const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000)
// 相机在three.js三维坐标系中的位置
camera.position.set(292, 223, 185)
// 相机观察目标指向three.js坐标系原点
camera.lookAt(0, 0, 0)

/*
  创建渲染器对象
*/
const renderer = new THREE.WebGLRenderer()
// 设置three.js渲染区域的尺寸(px)
renderer.setSize(width, height)
// 执行渲染操作
renderer.render(scene, camera)
// three.js执行渲染命令会输出一个canvas画布,也就是一个HTML元素,可以插入到body中
document.body.appendChild(renderer.domElement)

效果:

image.png

三、添加坐标轴

在renderer.render方法前添加

const axesHelper = new THREE.AxesHelper(150)
scene.add(axesHelper)

同时设置材质的透明为true,透明度为0.7

const material = new THREE.MeshBasicMaterial({
  color: 0x00ffff,
  transparent: true,
  opacity: 0.7
})

红绿蓝分别表示X、Y、Z轴

image.png

四、光源对物体表面影响

前面用到的MeshBasicMaterial是基础网格材质,不受光照影响;改为MeshLambertMaterial漫反射网格材质,它会受光照影响

const material = new THREE.MeshLambertMaterial({
  color: 0x00ffff
})

此时,物体看不见了,这是因为还没有设置光源

image.png

/*
  设置光源
*/
// 点光源-像一个灯泡
const ponitLight = new THREE.PointLight('white', 1.0)
// 光源强度不随着距离增加而衰减
ponitLight.decay = 0.0
// 点光源在坐标系中的位置
ponitLight.position.set(400, 100, 200)
// 点光源添加到场景中,在render之前
scene.add(ponitLight)

image.png

五、轨道控制器OrbitControls

// 引入轨道控制器OrbitControls
import { OrbitControls } from './jsm/controls/OrbitControls.js'
// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, renderer.domElement)
// 监听相机变化,重新渲染
controls.addEventListener('change', () => {
  renderer.render(scene, camera)
})
  1. 鼠标左键:旋转
  2. 鼠标中键:缩放
  3. 鼠标右键:平移

动画.gif

六、环境光与平行光

模拟点光源

// 可视化点光源
const helper = new THREE.PointLightHelper(ponitLight, 10)
scene.add(helper)

动画.gif

添加环境光与平行光

// 环境光
const ambient = new THREE.AmbientLight('white', 0.4)
scene.add(ambient)

// 平行光
const directional = new THREE.DirectionalLight('white', 1)
directional.position.set(100, 200, 150)
scene.add(directional)

image.png

可视化平行光

// 可视化平行光
const directionalLightHelper = new THREE.DirectionalLightHelper(
  directional,
  5,
  'deeppink'
)
scene.add(directionalLightHelper)

image.png

七、循环动画渲染

requestAnimationFrame的渲染比定时器更高效和流畅

// 渲染循环
const clock = new THREE.Clock() // 创建一个时钟对象
function render() {
  const spt = clock.getDelta() * 1000
  console.log('spt', spt)
  console.log('每秒渲染次数', 1000 / spt)
  renderer.render(scene, camera) // 执行渲染操作
  mesh.rotateY(0.01) // 每次绕y轴旋转0.01弧度
  requestAnimationFrame(render) // 请求动画帧,理论上每秒执行60次
}
render()

// setInterval(() => {
//   const spt = clock.getDelta() * 1000
//   console.log('spt', spt)
//   console.log('每秒渲染次数', 1000 / spt)
//   renderer.render(scene, camera) // 执行渲染操作
//   mesh.rotateY(0.01) // 每次绕y轴旋转0.01弧度
// }, 100);

动画.gif

如果循环渲染相机轨道控制器OrbitControls一起使用,循环函数render中会执行渲染操作,此时监听相机变化可以去掉

// 监听相机变化,重新渲染
// controls.addEventListener('change', () => {
//   renderer.render(scene, camera)
// })

八、全屏渲染和屏幕大小动态变化

全屏渲染只需要将原来写死的宽高改为动态获取窗口的宽高即可

const width = window.innerWidth
const height = window.innerHeight

resize事件处理:

window.onresize = () => {
  // 重新渲染canvas画布尺寸
  renderer.setSize(window.innerWidth, window.innerHeight)
  // 全屏情况下:设置观察范围长宽比aspect为窗口宽高比
  camera.aspect = window.innerWidth / window.innerHeight
  // 更新相机投影矩阵
  camera.updateProjectionMatrix()
}

九、性能监视器stats

导入

import Stats from './jsm/libs/stats.module.js'

实例化并插入到body中

const stats = new Stats()
document.body.appendChild(stats.domElement)

渲染函数中添加stats.update()

  stats.update() // 刷新时间

image.png

十、阵列立方体

用循环创建网络模型对象,并添加到场景中

for (let i = 0; i < 10; i++) {
  for (let j = 0; j < 10; j++) {
    const mesh = new THREE.Mesh(geometry, material)
    mesh.position.set(i * 100, 0, j * 100)
    scene.add(mesh)
  }
}

image.png

十一、常见的几何体

长方体

// 长方体 长宽高都是20
const geometry = new THREE.BoxGeometry(20, 20, 20)
image.png

球体

const geometry = new THREE.SphereGeometry(20)
image.png

圆柱

const geometry = new THREE.CylinderGeometry(20, 30, 50)
image.png

矩形平面

const geometry = new THREE.PlaneGeometry(40, 20)
image.png

材质默认正面可见,如果想要双面可见,设置side: THREE.DoubleSide

圆形平面

const geometry = new THREE.CircleGeometry(20)
image.png

十二、高光网络材质

const material = new THREE.MeshPhongMaterial({
  color: 0x00ffff,
  shininess: 20, // 亮度,默认30
  specular: 'white' // 高光部分的颜色
})
image.png

十三、渲染器设置(缓解锯齿模糊)

const renderer = new THREE.WebGLRenderer({
  antialias: true // 是否启用抗锯齿
})
// 告诉threejs,当前屏幕的设备像素比
renderer.setPixelRatio(window.devicePixelRatio)

十四、gui

gui基本语法(改变环境光)

导入并实例化

import { GUI } from './jsm/libs/lil-gui.module.min.js'

const gui = new GUI()
gui.domElement.style.right = 0
gui.domElement.style.width = '300px'

add方法添加环境光的ui交互界面

// 环境光
const ambient = new THREE.AmbientLight('white', 0.4)
scene.add(ambient)
gui.add(ambient, 'intensity', 0, 2)
动画.gif

命名

环境光和点光源属性名都是intensity,此时可以使用name方法命名

gui.add(ambient, 'intensity', 0, 2).name('环境光')
gui.add(ponitLight, 'intensity', 0, 10).name('点光源')

image.png

步长

gui.add(ambient, 'intensity', 0, 2).name('环境光').step(0.1)

事件

gui
  .add(ambient, 'intensity', 0, 2)
  .name('环境光')
  .step(0.1)
  .onChange(val => {
    console.log({ val })
  })

颜色

设置材质颜色

gui.addColor(ambient, 'color').onChange(val => {
  mesh.material.color.set(val)
})

下拉菜单

从上面可以知道,add方法的参数三和参数四是一个数字,交互界面是一个拖动条; 如果参数三是一个数组或者对象,交互界面会生成一个下拉菜单

gui.add(ponitLight, 'intensity', [-10, 1, 10]).name('点光源')

const obj = { distance: 0 }
gui
  .add(obj, 'distance', { left: -10, center: 0, right: 10 })
  .name('方位选择')
  .onChange(val => {
    mesh.position.x = val
  })

image.png

单选框

add方法改变的属性是一个布尔值,那么交互界面会生成一个单选框

const obj = { distance: 0, bool: true }
gui.add(obj, 'bool').name('是否旋转')


// 渲染函数中需要给mesh.rotateY(0.01)加上判断条件
function render() {
  renderer.render(scene, camera) // 执行渲染操作
  obj.bool && mesh.rotateY(0.01) // 每次绕y轴旋转0.01弧度
  requestAnimationFrame(render) // 请求动画帧,理论上每秒执行60次
}
render()
动画.gif

分组

不分组的情况

const obj = {
  color: 'deeppink', // 材质颜色
  specular: 'red' // 材质高光颜色
}

gui
  .addColor(obj, 'color')
  .name('材质颜色')
  .onChange(val => {
    material.color.set(val)
  })
gui
  .addColor(obj, 'specular')
  .name('材质高光颜色')
  .onChange(val => {
    material.specular.set(val)
  })
// 环境光强度
gui.add(ambient, 'intensity', 0.2, 1)
// 平行光强度
gui.add(directional, 'intensity', 0.2, 10)
// 平行光位置
gui.add(directional.position, 'x', -400, 400)
gui.add(directional.position, 'y', -400, 400)
gui.add(directional.position, 'y', -400, 400)

image.png

使用addFolder创建菜单

// 创建材质菜单
const matFolder = gui.addFolder('材质')
matFolder.addColor(obj, 'color').onChange(val => {
  material.color.set(val)
})
matFolder.addColor(obj, 'specular').onChange(val => {
  material.specular.set(val)
})

// 创建环境光菜单
const ambientFolder = gui.addFolder('环境光')
ambientFolder.add(ambient, 'intensity', 0, 2)

// 创建平行光菜单
const dirFolder = gui.addFolder('平行光')
dirFolder.close()
dirFolder.add(directional.position, 'x', -400, 400)
dirFolder.add(directional.position, 'y', -400, 400)
dirFolder.add(directional.position, 'z', -400, 400)

image.png