在竖直方向上渲染笔刷

原文作者:meded90
原文链接:https://discourse.threejs.org/t/rendering-brush-when-direction-straight/93562
编译说明:本文基于 three.js 官方论坛原帖进行深度编译与解读,保留核心代码,并针对实现中的逻辑缺陷、性能隐患与术语规范进行技术剖析。


1. 问题背景与原帖内容

原作者在 three.js 论坛发帖,描述了这样一个需求:

当方向是“笔直”(straight)的时候,尝试渲染一个“笔刷”(brush),预期该笔刷能够被正确渲染出来。

原帖给出的关键代码片段如下(已保留原样):

takeFromPassdThroughObjective(
  sampleBrushRectIntoPassThroughDirection,
  "sample-pick-profile",
).getWorldPosition(intoBrushWC);

以及条件判断:

if (Number(intoBrushWC.z) * prime_profile === Number(intoBrushWC.z) * 1) {
  // keep
} else {
  return; /* keep */
}

随后执行:

scene.add(samplePickBrushFromPassd);

作者声称:当方向为“笔直”时,条件 Number(intoBrushWC.z) * prime_profile === Number(intoBrushWC.z) * 1 会被满足,于是笔刷得以保留并添加到场景中。

从代码片段来看,作者尝试通过世界坐标的 z 分量来判断笔刷方向是否笔直,但整个实现存在多处逻辑、性能和可读性问题。


2. 代码逐段解析

2.1 获取世界坐标

takeFromPassdThroughObjective(
  sampleBrushRectIntoPassThroughDirection,
  "sample-pick-profile",
).getWorldPosition(intoBrushWC);
  • 这里调用了一个自定义函数 takeFromPassdThroughObjective(...),其命名疑似拼写有误(Passd 应为 Passed,Objective 可能指 Object)。
  • 该函数接收两个参数:sampleBrushRectIntoPassThroughDirection(一个对象或矩形区域)和一个字符串标识 "sample-pick-profile"。
  • 返回值调用 getWorldPosition(intoBrushWC),这是 three.js 中 Object3D 的常用方法,用于将对象的世界坐标写入传入的向量 intoBrushWC。

问题点:

  • getWorldPosition 会在每次调用时通过父级变换矩阵计算世界坐标,若每帧都调用,会带来不必要的矩阵乘法开销。
  • intoBrushWC 是一个外部传入的向量,但代码中没有看到其初始化,容易导致变量泄漏或未定义。

2.2 条件判断

if (Number(intoBrushWC.z) * prime_profile === Number(intoBrushWC.z) * 1) {
  // keep
} else {
  return; /* keep */
}
  • 该条件比较 Number(intoBrushWC.z) * prime_profile 与 Number(intoBrushWC.z) * 1 是否相等。
  • 这个表达式等价于:prime_profile === 1(假设 Number(intoBrushWC.z) 非零且不为 NaN,并且 prime_profile 为数值类型)。
  • 因此,无论笔刷方向如何,只要 prime_profile 等于 1,条件就恒为 true;否则恒为 false。
  • 也就是说,该条件与方向是否“笔直”完全无关,逻辑上存在严重缺陷。
  • else 分支中的 return /* keep */ 实际上什么也不做,return 后接注释,相当于直接结束当前函数或作用域,但注释 /* keep */ 有误导性,实际并未“保留”任何对象。

2.3 场景添加

scene.add(samplePickBrushFromPassd);
  • 无论上述条件判断结果如何(甚至可能被跳过),都会执行 scene.add。
  • 如果该对象已经在场景图中,重复调用 scene.add 只会重新添加同一对象(three.js 内部会先移除再添加,但会产生额外开销),而不会产生多个副本。
  • 但更关键的问题是:条件判断没有真正控制对象是否被添加到场景,导致逻辑失效。

3. 三个核心优化要点

优化 1:修正方向判断逻辑,使用向量点积或夹角检测

当前代码使用 intoBrushWC.z 与 prime_profile 的乘法比较,与“方向笔直”没有任何数学关系。正确做法是:

  • 获取笔刷的方向向量(例如从起点到终点的向量,或对象的 forward 向量)。
  • 将其与参考轴(如世界坐标的 Y 轴或 Z 轴,取决于“笔直”的定义)进行点积运算。
  • 根据点积结果判断是否足够“笔直”,例如 direction.normalize().dot(up) > 0.95 表示与上方向几乎平行。

示例修正:

const direction = new THREE.Vector3();
// 假设 brushTarget 是终点,brushOrigin 是起点
direction.subVectors(brushTarget, brushOrigin).normalize();

const up = new THREE.Vector3(0, 1, 0);
if (direction.dot(up) > 0.98) {
  // 方向接近笔直向上,保留笔刷
} else {
  // 方向偏离,不渲染
}

这样可以真正判断方向是否“笔直”,且性能更优(点积比多次乘法比较更直接)。

优化 2:避免重复调用 scene.add,改用可见性控制

scene.add 只应在对象创建时调用一次。若需要控制显示与隐藏,应使用 object.visible 属性:

// 初始化时
scene.add(samplePickBrushFromPassd);

// 每帧或逻辑判断时
samplePickBrushFromPassd.visible = isStraightDirection;

这样可以避免场景图重复增删带来的开销,并使逻辑清晰。

优化 3:缓存世界坐标,避免每帧重复计算

getWorldPosition 涉及矩阵运算,如果在每帧或高频事件中反复调用,会浪费性能。建议:

  • 在对象变换不变的情况下,缓存世界坐标;
  • 或者仅在需要时更新,并通过事件驱动(如用户移动物体后)更新缓存;
  • 更好的方式:直接使用局部坐标进行方向判断,因为局部坐标计算更廉价且不需要遍历父级矩阵。

示例:

// 使用局部坐标判断,避免频繁 getWorldPosition
const localDirection = new THREE.Vector3(0, 0, 1); // 假设笔刷局部方向为 +Z
const worldDirection = localDirection
  .clone()
  .transformDirection(object.matrixWorld);
if (Math.abs(worldDirection.y) > 0.98) {
  // 笔直向上
}

这里 transformDirection 仅在需要时使用矩阵,避免重复写回世界坐标向量。


4. GLSL / WebGL 专业术语检查

本段原文并未涉及 GLSL 着色器代码,但涉及 WebGL / three.js 的相关术语。以下为术语检查与规范对照:

术语(英文)中文规范译法备注
WebGLWebGLWeb 图形库,three.js 的底层渲染 API
three.jsthree.jsJavaScript 3D 库,不翻译
scene graph场景图three.js 中组织对象层级的数据结构
world coordinates世界坐标getWorldPosition 返回的坐标空间
render / rendering渲染绘制 3D 对象到屏幕的过程
brush笔刷 / 画刷在 3D 建模/雕刻语境下通常译为“笔刷”
pass-through传递 / 穿透原代码 PassdThroughObjective 疑为拼写错误
profile轮廓 / 剖面依赖上下文,此处可能指笔刷的截面形状
vector向量数学概念
dot product点积用于判断向量方向相似度
normalization归一化将向量长度缩放到 1
shader着色器原文未涉及,但属于 GLSL 核心术语
vertex / fragment shader顶点 / 片元着色器原文未涉及,GLSL 常用术语
uniform / varyinguniform / varyingGLSL 变量类型,原文未涉及

结论:原文没有出现任何 GLSL 着色器语言代码或术语,但涉及 WebGL 渲染相关的 three.js 概念。在编译时,上述术语均应采用规范中文译名,并保留必要英文原词。


5. 总结

该论坛帖子提出了一个“根据方向是否笔直来渲染笔刷”的需求,但实现代码存在明显缺陷:

  • 条件判断与方向无关,逻辑失效;
  • scene.add 重复调用,且未正确控制显示;
  • 变量命名混乱,可读性差;
  • 世界坐标获取方式可能带来性能浪费。

通过本文的深度解读,我们提炼出三个核心优化方向:使用向量点积判断方向、用 visible 替代重复 scene.add、缓存或局部计算坐标,从而让代码逻辑正确且性能更优。同时,我们对涉及的 WebGL / three.js 专业术语进行了规范检查,确保编译内容准确、专业。


本文为深度解读,非逐字翻译。原文作者为 meded90,原始链接见文首。