当前位置: 首页 > news >正文

three.js+WebGL踩坑经验合集(10.2):镜像问题又一坑——THREE.InstancedMesh的正反面向光问题

本文紧接上一篇内容:three.js+WebGL踩坑经验合集(10.1):镜像问题又一坑——THREE.InstancedMesh的正反面显示问题-CSDN博客

上篇笔者提到了THREE.InstancedMesh这个用于优化渲染性能的对象,并且给大家分享了在使用过程中遇到的一个跟镜像有关的坑。最后给出了把显隐弄对的方案:把有镜像和没镜像的实例放到不同的THREE.InstancedMesh里面并且把镜像了的实例的material.side设置为BackSide。这样的话,显隐是对了,但是光照效果还有问题。

通过上文,我们不难发现,问题的根源在于一旦往InstancedMesh.setMatrixAt传入负定矩阵,不管你用的是BackSide还是FrontSide,它都始终显示暗面(背光面)。

所以一个很好的规避方案,是把负定矩阵的“负变换部分”从setMatrixAt转移到mesh的matrixWorld上。(这里不再给出完整代码,读者可以本篇开头给的上一篇博文链接获取)

var material_mirror = material.clone();
material_mirror.side = THREE.FrontSide;
var instancedMesh_mirror = new THREE.InstancedMesh(geometry, material_mirror, instanceCount >> 1);
instancedMesh_mirror.scale.x = -1;
var scaleMatrix = new THREE.Matrix4();
scaleMatrix.makeScale(-1, 1, 1);
for(var i = instanceCount >> 1; i < instanceCount; i ++)
{let matrix = matrixes[i].clone().premultiply(scaleMatrix);instancedMesh_mirror.setMatrixAt(i - (instanceCount >> 1), matrix);
}
scene.add(instancedMesh_mirror);

我们给InstancedMesh本身设置一个负缩放,并且把负缩放变换应用到THREE.InstancedMesh上的每一个实例。由于THREE.InstancedMesh就是THREE.Mesh的子类,也不像THREE.Line2那样通过shader计算顶点,所以它会跟普通Mesh一样工作得很好。并且这个时候连BackSide都不需要了。

我们再开启下双面观察它的背光面颜色是否正常。

读者还可以自行尝试BackSide的效果。

这个扬长避短的做法比较蹩脚,但是如果项目对这些变换的处理维护到位,那也不失为一个完美的解决方案(注意这个完美不用带引号)。

下面我们就来探讨下,为什么给InstancedMesh的实例设置负定矩阵,并且material.side调整为BackSide之后,向光面会变成背光。

可能有的读者会觉得,既然都改成BackSide了,那朝着屏幕的是背光面不很正常嘛。嗯,这样子理解确实没毛病,只不过,我们现在遇到的问题是,同样的材质和矩阵,用普通渲染和用THREE.InstancedMesh出来的结果不一致。所以最起码要搞清楚两者的差异。

笔者研读了下跟MeshLambertMaterial和DirectionLight(平行光)相关的代码,其光照的实现原理很直观(不考虑遮挡,阴影这些),用的是物体法线和光线的夹角余弦值(向量点乘),0度最亮,180度最暗。

所以出现了向光背光问题是因为法线反了?不像,因为笔者全程都是通过改x方向的负缩放来测试的。

那么现在,我们把提取负定矩阵的部分回退下,改回BackSide来深入探讨InstancedMesh出现问题的原因。

var instanceCount = matrixes.length;
material.side = THREE.FrontSide;
var instancedMesh = new THREE.InstancedMesh(geometry, material, instanceCount >> 1);
for(var i = 0; i < instanceCount >> 1; i ++)
{instancedMesh.setMatrixAt(i, matrixes[i]);
}
scene.add(instancedMesh);
var material_mirror = material.clone();
material_mirror.side = THREE.BackSide;
var instancedMesh_mirror = new THREE.InstancedMesh(geometry, material_mirror, instanceCount >> 1);
for(var i = instanceCount >> 1; i < instanceCount; i ++)
{let matrix = matrixes[i].clone();instancedMesh_mirror.setMatrixAt(i - (instanceCount >> 1), matrix);
}
scene.add(instancedMesh_mirror);

下面我们就基于这个结果去做深入的探讨。

首先,给material的side设置THREE.BackSide之后,three.js会给shader加上FLIP_SIDED的定义。

然后笔者找到了使用FLIP_SIDED这一定义的地方:

这个地方,作者的意图相当明确,就是当物体需要显示背面的时候,就把法线取反,来区分背光面和向光面的效果。

那难道笔者发现的这个bug是设计如此?笔者试着去掉这个取反的变换,看看结果。

妈呀,还真是。BackSide就是要故意违背物理规律显示暗面的?!

不对,普通渲染的时候不是正常的?我们试试改回普通渲染(设置useInstancing=false),看看BackSide的效果(修改false分支上的部分)。

var mesh = new THREE.Mesh(geometry, material);
material.side = THREE.BackSide;
mesh.matrix.copy(matrix);
mesh.matrix.decompose(mesh.position, mesh.quaternion, mesh.scale)
scene.add(mesh);

好了,坏掉了,这回怎么样都解释不通,不管是光照方向还是side设置,显示成向光面效果都是错误的,所以这个地方一定还有坑。我们跑去meshlambert的shader代码里看看。

这个地方有区分正面和反面的实现代码,然后笔者想起来前面在找灯光计算的地方,也有这些变量的存在。

观察下来发现back和front的值是互为相反数,并且back只用在了双面的处理上。也就是说,在这个位置上,不管用BackSide还是FrontSide,结果都是取的vLightFront来做的光照叠加。

收集到了这些线索之后,我们分别看看使用镜像变换并且使用BackSide时,普通渲染跟合批渲染的操作逻辑。

先来看普通渲染。我们先假定初始时用于计算的灯光向量和法线的方向一致,夹角小于90度,物体会变亮。

1 object.matrixWorld.determinant() < 0,渲染的时候会把FrontFace改掉,面绕序和FrontFace相互抵消,相机依然要旋转到背面才可见。

2 shader的FLIP_SIDED开启,法线会被取反,灯光向量和法线的方向变得不一致。

3 旋转镜头到背面,面片可见了,显示的也是变暗的颜色,跟预期结果相吻合。

再来看合批渲染,负定矩阵在实例里

1 object.matrixWorld.determinant() > 0,渲染的时候不会把FrontFace改掉

2 InstancedMesh的getMatrixAt的determinant() < 0,面片的面绕序改了,但是因为FrontFace维持原样,所以面片初始化就可见了。

3 此时shader的FLIP_SIDED还是开启的,法线还是取反,所以灯光向量和法线的方向照样不一致,这时候,显隐是错的,但是光照结果正确。

4 如果这时候把material.side改成FrontSide,则显隐正确但光照结果错误。相机转到背面可见,但是显示的是亮面,因为法线没取反。

这里我们看到的差异点依然是负定矩阵所在的位置,然后似乎存在运气成分,就是frontFace,法线取反有没抵消,抵消成功则结果正确,否则错误。

想了想,materialSide改成BackSide,可见的面方向的确发生了更改,所以在shader上给法线取反的做法是正确的,我们硬是把BackSide改成FrontSide,法线就是不对的,完全是针对显隐问题而做的补丁。

笔者在写这篇博文之前,是通过改Shader代码来打的补丁,因为笔者发现,算灯光向光背光的地方,只做了双面和单面的区分,而没有区分正面和反面。

然后笔者尝试在这里多加一个FLIP_SIDED的分支:

为什么还要区分是不是InstancedMesh呢?因为不是InstancedMesh的情况下,这个地方本来就是对的,所以还特地这样子写了下。实际上,如果THREE.InstancedMesh本身(非实例)带了负矩阵的话,那这样子改还是不对的。但是考虑到我们的项目不会对THREE.InstancedMesh设置负矩阵(这个操作本来就骚),所以就这样凑合着过去了。

这里的做法是,在对THREE.InstancedMesh使用BackSide时,取背光一面的颜色进行渲染,从而抵消掉FLIP_SIDED对法线的取反操作。

现在看起来,这种补丁更应该像处理Line2那时候的方式一样,业务根据THREE.InstancedMesh的实例是否使用了负定矩阵来进行设置。因为问题的根源是镜像后的面绕序没有修正,而非法线的正反问题,也不是面的正反问题。

这样子做之后,在假定一个InstancedMesh里面的所有实例的矩阵的佚的符号一致的情况下,问题是可以得到根治。唯一不好的地方在于每次渲染都要去算这个Matrix,性能会下降。我们用InstancedMesh不就是为了优化性能嘛,这样做值得么?

所以,针对业务,是可以有优化方案的,比如我们给InstancedMesh或者material加个实例是否为负矩阵的标记,这一做法可根据项目代码的结构而进行更合理的实现,这里就不展开聊了。

下面我们来小结一下:

1 物体自身的矩阵为负定矩阵时,three.js会对FrontFace取反,确保显隐正确

2 material.side在设置为BackSide时,shader内部会对法线进行取反,确保向光和背光的结果正确

3 THREE.InstancedMesh的实例为负定矩阵时,three.js没有做任何处理,导致FrontFace不正确,面的显隐出错

4 笔者尝试通过提取负定矩阵到自身,规避实例的负定矩阵,可以比较好的解决问题,但是这会增加业务层的维护难度。

5 然后笔者又尝试通过把法线取反去掉,以及把向光和背光面互换等方式来调整光照结果,虽然在特定的情况下能解决问题,但这都是针对FrontFace不正确而打的补丁,也是不可取的

6 最后笔者针对负定矩阵的丢失问题,在更合理的位置对FrontFace进行修正,结果就很好了(当然了,正负定矩阵混在一个InstancedMesh里面的情况还是不对,但混合正负矩阵的做法真心不建议搞),并且指出这种方法比较吃性能,可以针对项目代码结构自行优化。

在处理BackSide的过程中,我们看到了双面材质又有特定的代码进行处理。这里也有一个比较大的坑,是gl_FrontFacing的问题。

不过这篇就到此为止吧,毕竟内容已经够多,够大家消化一整天了,我们后面再继续探讨,辛苦大家了!

http://www.dtcms.com/a/361836.html

相关文章:

  • 亥姆霍兹线圈和放载流线圈
  • 【SpreadJS V18.2 新特性】Table 与 DataTable 双向转换功能详解
  • SD卡自动检测与挂载脚本
  • React 第七十一节 Router中generatePath的使用详解及注意事项
  • table表格字段明细展示
  • 【前端教程】ES6 Promise 实战教程:从基础到游戏案例
  • django的URL路由配置常用方式
  • C# Task 入门:让你的程序告别卡顿
  • 基于STM32单片机的无线鼠标设计
  • 【ComfyUI】图像反推描述词总结
  • 杰理ac791无法控制io脚原因
  • 【算法】算法题核心类别与通用解题思路
  • 时序数据库IoTDB:为何成为工业数据管理新宠?
  • 【frontend】w3c的发展历史ToDo
  • accelerate、trainer、lightning还是pytorch?
  • SpringBoot 分库分表 - 实现、配置与优化
  • 雅思听力第四课:配对题核心技巧与词汇深化
  • CLion编译基于WSL平台Ubuntu系统的ros项目
  • 1.人工智能——概述
  • 测试开发的角色
  • 动态规划:硬币兑换II
  • 异常类分析
  • HTML应用指南:利用GET请求获取全国招商银行网点位置信息
  • 软件测试面试技巧-面试问题大全
  • 盟接之桥说制造:守正出奇:在能力圈内稳健前行,以需求导向赢得市场
  • 综合实验:DHCP、VLAN、NAT、BDF、策略路由等
  • 数据库主键选择策略分析
  • 【高级】系统架构师 | 2025年上半年综合真题
  • Linux系统结构(概要)
  • 实现一个线程池管理器