Three.js 之使用 dat.gui 创建调试工具

1,037 阅读7分钟

浏览 three.js 官方文档时,有些页面会有如下图所示的我们可以亲手调整参数直接查看对应效果的调试工具:

1.png

本篇文章以之前使用 threejs 创建的旋转立方体项目为基础,为了方便查看效果,我将材质由原来的 MeshLambertMaterial 改为了 MeshPhongMaterial,以模拟具有镜面高光的光泽表面,另外去除了环境光和作为地板的平面以及阴影,以便更加专注于本文的重点 —— 我们自己如何创建出这么一个可视化调参工具(后文简称为“控件”)。

dat.gui 的使用

引入

该工具其实就是使用 dat.gui 这个库创建的,可以不单独安装而是直接引入,因为 threejs 内置了 dat.gui 相关 js 文件:

import * as dat from 'three/examples/jsm/libs/lil-gui.module.min.js'

也可以通过 pnpm 单独安装,由于使用了 ts,为了更方便获取接口提示等功能,还需要安装上相关类型声明文件:

pnpm add dat.gui
pnpm add @types/dat.gui -D

在项目中导入 dat.gui,就能够在全局打印查看 dat 对象了

import * as dat from 'dat.gui'
console.log(dat)

打印结果如下:

2.png

初步体验

dat.gui 的官方介绍有这么一句话:

A lightweight graphical user interface for changing variables in JavaScript.

也就是说它是用来创建一个可以改变 js 变量的用户界面,我们可以新建一个控制对象 controlData,该对象的属性即为可以通过控件改变的变量,并给它们赋上默认值:

const controlData = {
  opacity: 1,
  side: 'front',
  wireframe: false,
  color: '#ff0000',
}

然后新建一个 gui 实例:

const gui = new dat.GUI()

通过 guiaddFolder 方法添加分组,返回的 folder 依旧是类型为 GUI 的实例:

const folder = gui.addFolder('立方体')

传入的参数 '立方体',会显示在如下图所示的位置:

3.png

再在分组下通过实例的方法添加需要控制的属性,本文介绍 2 个常见的方法 addaddColor

add

add 方法的前 2 个参数固定不变:

  • 第 1 个参数为控制对象 ,即 controlData
  • 第 2 个参数为需要控制的属性的 key 值;

之后的参数则根据调整方式的不同可以分为以下几种情况:

滑动取值

如果值在一定范围内改变,比如透明度 opacity 就可以在 0 到 1 的范围内调整,则可以再传入 3 个参数,依次为最小值、最大值和每次调整的幅度:

folder.add(controlData, 'opacity', 0, 1, 0.1).onChange(value => {
  // 这里可以用最新的 value 去修改 three.js 的相关参数
})

1.gif

add() 和稍后会介绍的 addColor() 后都可以跟上 onChange(),其参数是一个回调函数,当我们通过生成的控件调整某个属性的值时,最新值会作为参数(value)传给回调函数。

下拉选择

如果值是固定的几个可选项,第 3 个参数就传入一个数组,数组里的元素就是下拉选择框里的可选项:

folder.add(controlData, 'side', ['front', 'back', 'double'])

2.gif

勾选框

如果值是布尔类型的,则无需传入第 3 个参数,呈现在控件上的会是一个勾选框:

folder.add(controlData, 'wireframe')

3.gif

addColor

如果值是颜色,需要有个颜色选择器,则直接使用 addColor 方法:

folder.addColor(controlData, 'color')

4.gif

最后,如果要让分组为默认展开状态,则需要添加:

folder.open()

结合 three.js

我们封装一个函数 initDatGui 来处理 dat.gui 与 three.js 的结合使用。在 three.js 构建的场景中,可能同时用到了灯光、材质、几何体等各种不同的实例对象,所以我们让函数的第 1 个参数是个数组 controls,数组中的元素为对象,一个对象代表一个分组,对象拥有 2 个属性:

  • name 属性的值为分组名,传递给 gui.addFolder()
  • target 属性的值则为需研究的 three.js 中的实例对象。

下面以调试聚光灯为例进行说明。

获取实例属性

聚光灯实例对象 spotLightnew THREE.SpotLight(0xffffff, 300) 得到,查看 SpotLight构造方法 constructor,可以看到聚光灯实例有 6 个属性:

4.png

我们想实现的效果是,如果要调试聚光灯,只需如下所示在传递给 initDatGui 的第 1 个参数的数组里添加上对象:

initDatGui([
  {
    name: '聚光灯',
    target: spotLight
  }
])

dat.gui 控件就会新增上一个名为 '聚光灯' 的分组,分组里会有聚光灯的所有属性,可供调整。 那么我们要如何通过 spotLight 获取到它的所有属性呢?打印查看 spotLight,可以看到它有个 type 属性,值为 'SpotLight'

5.png

我们可以定义一个对象 threeObj 用来存放需要研究的 three.js 中的类(如 SpotLight)与对应的实例属性:

const threeObj = {
  SpotLight: ['color', 'intensity', 'distance', 'angle', 'penumbra', 'decay'],
}

现在,我们就可以通过 const props = threeObj[target.type] 获取到聚光灯所有属性组成的数组 props 了。

属性的取值与赋值

接下来就要考虑两个问题:

  1. 我们如何获取初始状态时,聚光灯中的某个属性的默认值,然后让 dat.gui 正确显示?
  2. 如果我们通过 dat.gui 生成的控件改变了某个属性的值,要如何让 three.js 生成的聚光灯做出正确的反应?

取值

对于 color 属性的获取,如果我们通过聚光灯实例直接获取 color

console.log(spotLight.color)

打印结果如下:

6.png

这是无法作为 dat.gui 里颜色选择器的默认值的,我们还需链式调用 getStyle() 做进一步的处理,得到字符串类型的颜色值:

console.log(spotLight.color.getStyle()) // rgb(255,255,255)

聚光灯中的 intensity 等其它属性,则可以直接通过实例获取,比如获取亮度:

console.log(spotLight.intensity) // 300

赋值

我们可以通过 spotLight.color.setStyle() 传入一个 css 中的颜色样式字符串来改变聚光灯的 color 属性:

spotLight.color.setStyle('rgb(255,129,0)')

但是对于聚光灯中的其它属性的赋值,则可以直接通过等号赋值,比如:

spotLight.intensity = 100

鉴于不同的属性可能有着不同的取值与赋值的方法,并且添加进入 dat.gui 的方法也可能不同,所以我们还需要定义一个 propOptions 如下:

const propOptions = {
  color: {
    method: 'addColor',
    getValue: target => target.color.getStyle(),
    setValue: (target, value) => target.color.setStyle(value)
  },
  intensity: {
    rest: [0, 1000],
    getValue: target => target.intensity,
    setValue: (target, value) => target.intensity = value
  },
  distance: {
    rest: [0, 2],
    getValue: target => target.distance,
    setValue: (target, value) => target.distance = value
  },
  angle: {
    rest: [0, Math.PI / 2],
    getValue: target => target.angle,
    setValue: (target, value) => target.angle = value
  },
  penumbra: {
    rest: [0, 1],
    getValue: target => target.penumbra,
    setValue: (target, value) => target.penumbra = value
  },
  decay: {
    rest: [0, 4],
    getValue: target => target.decay,
    setValue: (target, value) => target.decay = value
  }
}
  • method 用于记录该属性如何添加进 dat.gui 生成的控件,默认为 'add'
  • 前文提到了 add 方法的前 2 个参数固定,所以rest 代表传入 add 的除前 2 个参数之外的剩余参数,它的值是一个数组:
    • 如果数组传入 2 个数字则代表是滑动取值,比如 [0, 1000] 表示取值范围为 0~1000;
    • 如果传入数组的也是一个数组,则表示是下拉选择,比如 [['front', 'back', 'double']]
    • 如果什么也不传入数组,即 reset 值为空数组,则表示是勾选框。
  • getValue 值为函数,用于在页面初始化时获取当前属性的默认值。调用时传入的参数 target 即为传入 initDatGui 的第一个参数的数组中,某个对象的 target
  • setValue 值也为函数,用于将属性的最新值更新到 three.js 渲染的场景中。调用时传入 target 和属性的最新值 value

代码演示

现在,我们就可以遍历 props,从 propOptions 中得到对应属性的对象(propObj),从而能进一步给控制对象 controlData 里的对应属性赋上默认值,并在控件被调整触发了 onChange 方法中的回调时,将最新的属性值更新到渲染场景中。代码演示如下:

感谢.gif 点赞.png