three.js+WebGL踩坑经验合集(5.2):THREE.Mesh和THREE.Line2在镜像处理上的区别
本文紧接上篇:
(5.1):THREE.Line2又一坑:镜像后不见了
本文将解答上篇提到的3个问题,首先回答第二个问题,如何获取全局的缩放值。
scaleWorld这个玩意儿呢,three.js官方就没提供了。应该说,一般的渲染引擎都不会弄这个,而是把所有的变换都统一由matrixWorld来提供。
从矩阵提取缩放值,three.js也是提供了api,叫decompose(position:THREE.Vector3, quaternion:THREE.Quaternion, scale:THREE.Vector3)
使用者需要在外部创建好两个Vector3和一个Quaternion对象(四元数,处理旋转的,本文不展开聊),然后将其传入到对应的参数中,decompose方法将往这3个对象中写入值。
上文中的updateFace方法调整如下:
function updateFace(){
var position = new THREE.Vector3();
var rotation = new THREE.Quaternion();//这个我们无视,按类型要求传进去就是
var scale = new THREE.Vector3();
container.updateMatrixWorld();
line2.updateMatrixWorld();
line2.matrixWorld.decompose(position, rotation, scale);
line2Material.side = scale.x * scale.y * scale.z > 0 ? THREE.FrontSide : THREE.BackSide;
}
这样的做法比上文提到的要优雅一些,至少它的编码没那么硬了。
在回答剩下的两个问题之前,笔者先给大家简单介绍下原生WebGL处理正反面显示的两个重要的api
1 gl.cullFace(face)
渲染时需要剔除哪个面,有效值为
gl.BACK(绘制正面,剔除背面,默认),
gl.FRONT(绘制背面,剔除正面)
gl.FRONT_AND_BACK(正面和背面都剔除,这是画了个寂寞?本文不聊这个)
如果要开启双面,那就是什么都不剔除,此处没有一个值,而是用gl.enable(gl.CULL_FACE)和gl.disable(gl.CULL_FACE)代之。
2 gl.frontFace(clockwise)
WebGL底层通过三角面三个点在投影到屏幕上的顺逆时针顺序来定义正反面,默认设置为,顺时针代表正面,逆时针代表反面。然后可以通过该api去修改这一设置。
clockwise参数的有效值为gl.CW(顺时针,默认),gl.CCW(逆时针)
因为在上文的例子中,Mesh也是做了负缩放,也没开双面材质,按道理它镜像后是不可见的,所以three.js的底层会通过这两个处理正反面的api修复镜像后不能正确显示的问题。
笔者的摸索过程就不跟大家啰嗦了,直接告诉大家定位到代码在哪,一共3处
WebGLRenderer是渲染的核心类,但却看到了object.isMesh这样的补丁打在上面,所以架构上显得封装性不强,也不健壮(也许是性能使然?先不纠结这事)。我们可以看到,当被渲染的对象是Mesh的时候,正反面的逻辑依赖于世界矩阵的determinant函数返回值的符号。
determinant的实现代码如下:
如果你的线性代数还没完全还给老师的话,那这个式子你应该能看出来个所以然,就是矩阵M的行列式,数学概念叫秩,记为detM,det是determinant的简写。
如果你是个数学学霸,那大概还会记得正定矩阵和负定矩阵的概念。实际上,它可以准确匹配到物体的正负缩放上。其证明过程,笔者没有很轻易地通过搜索引擎获取得到,所以后面笔者会单开一篇文章给大家推导一遍。
言归正传,这里加了个补丁isMesh,那是不是再加个isLine2,问题就解决了?
很遗憾,事情没有想象中那么简单,因为Line2就是Mesh的子类:
那我们试试排除Line2?
测试发现,这样做的确可以把问题解决掉。
Line2之所以不应该跟随镜像调整正反面,是因为Line2虽然也是个矩形面片,但它的坐标值是动态计算的,先把端点的位置通过世界矩阵投影矩阵算好到屏幕的2D画布上,然后再向着固定的方向生成4个点,所以不管Line2缩放的符号是什么,4个点的绕序都是固定的。如果换成单面材质的PlaneMesh,那么你会发现相机旋转个180度之后,PlaneMesh就会看不见了,而且“线”的粗细还会随着镜头的移动而发生变化。
var line2Geometry = new THREE.PlaneGeometry(100, 5);
var line2Material = new THREE.MeshBasicMaterial({color: 0xFF6600});
var line2 = new THREE.Mesh(line2Geometry, line2Material);
line2.position.set(60, -15, 0);
line2.rotation.set(0, 0, Math.PI / 3);
container.add(line2);
综上所述就是,先算全局点再按粗细偏移出来的Line2面片,绕序不受矩阵影响。但是先把面片4个点确定下来再各自计算全局坐标的PlaneMesh,绕序就受矩阵影响了。
好了,这下笔者也打了补丁,这下打得更离谱,核心类去引用examples里面的特殊类型进行处理。笔者不忍心这样破坏它,就给Material加了一个属性,叫autoFlipFrontFace,默认为true,设置为false时不调整正反面的绕序。
然后给LineMaterial的这一属性设置为false。
写本文的时候,笔者再次审视这里的代码,认为更合理的做法是,在LineMaterial中通过判断matrixWorld的determinant值来控制面片4个顶点的生成方向。此法对架构的破坏力最小,但写起来相当麻烦,想要把封装性做好还要以牺牲性能为代价,放到博客上的可观赏性也很差,就干脆偷个懒好了。
下面来小结一下本文(包括上文):
1 THREE.Mesh,THREE.Line镜像后都能正常显示,唯独THREE.Line2会消失
2 THREE.Line走的是原生画线api,不受正反面问题的影响
3 THREE.Mesh和THREE.Line2都是面片
4 THREE.Mesh镜像后,面片的点绕序会发生变更,底层通过gl.frontFace进行修正
5 THREE.Line2镜像后,面片的点绕序不发生变更,但因为它继承了THREE.Mesh,所以也被误修了
6 镜像后的负缩放判断,除了用不地道的scale乘积,还可以用更硬核的determinant方法,数学上它是个行列式
小结完了,下篇笔者会跟大家专门探讨determinant可用于判断负缩放的原因,过程有点复杂,笔者会先从2D开始循序渐进,让大家的消化曲线趋于平缓。
明天就是除夕了,提前祝大家新春快乐,蛇全蛇美!