three.js+WebGL踩坑经验合集(6.2):负缩放,负定矩阵和行列式的关系(3D版本)

本篇将紧接上篇的2D版本对3D版的负缩放矩阵进行解读。

(6.1):负缩放,负定矩阵和行列式的关系(2D版本)

既然three.js对3D版的负缩放也使用行列式进行判断,那么,2D版的结论用到3D上其实是没毛病的,THREE.Line2出问题,是因为顶点生成规则跟Mesh不一致。而我们上篇提到的案例,则是用了2D平面的绕序来套到3D上,想必也是不合理的。

下面我们先针对上篇最后提到的问题做一个测试用例

<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>three_3Ddeterminant</title><style>body {margin: 0;overflow: hidden;}</style><script src="three/build/three.js"></script><script src="three/examples/js/controls/OrbitControls.js"></script>
</head><body><script>var scene = new THREE.Scene();var container = new THREE.Object3D();scene.add(container);var geometry = new THREE.PlaneGeometry(100, 50);var material = new THREE.MeshBasicMaterial({color: 0x993300, side:THREE.DoubleSide});var mesh = new THREE.Mesh(geometry, material);mesh.position.z = 100;container.add(mesh);var geometryBack = new THREE.PlaneGeometry(50, 100);var materialBack = new THREE.MeshBasicMaterial({color: 0x006600, side:THREE.DoubleSide});var meshBack = new THREE.Mesh(geometryBack, materialBack);meshBack.position.z = 90;container.add(meshBack);var width = window.innerWidth; var height = window.innerHeight; var camera = new THREE.PerspectiveCamera(60, width / height, 1, 20000);camera.position.set(0, 0, 500);  var renderer = new THREE.WebGLRenderer();renderer.setSize(width, height);renderer.setClearColor(0x000000, 1); document.body.appendChild(renderer.domElement); function render() {container.rotation.y += Math.PI / 180;renderer.render(scene, camera);requestAnimationFrame(render);}render();var controls = new THREE.OrbitControls(camera,renderer.domElement);controls.addEventListener('change', render);</script>
</body>
</html>

现在我给两个面片都开启了双面渲染,并且让面片所在的容器一直绕y轴旋转,让大家大致了解这个测试用例的结构。

真正测试的时候,我会把红色设置为正面,绿色设置为反面,效果如下。

可见,容器旋转到正面时,红色面显示绿色面隐藏,反过来则显示绿色面。

单改容器的rotation虽然按我们的理解是一个正定矩阵,但当它的rotation.y在90度到270度之间时,点的绕序确实发生了变更,所以也隐藏了,这跟我们在上篇2D版本上的结论不吻合。

然后我们再试试不旋转,而是改scale:

var t = 0;function render() {t += 3 * Math.PI / 180;container.scale.z = Math.sin(t);renderer.render(scene, camera);requestAnimationFrame(render);
}

缩放的情况,点的绕序没发生变更,但是正反面却也交替了。

是不是有点神奇?嗯看来是我们对3D绕序的理解不正确。

为了方便观察,我们把面片的xy向量绘制到容器上进行观察。与此同时,既然我们是研究3D的版本,那么我们直接用THREE.AxisHelper把z轴也一并绘制出来(红绿蓝分别代表xyz)。

var axisHelper = new THREE.AxisHelper(300);
axisHelper.position.z = 100;
container.add(axisHelper);

接下来,为了让每条轴在不同的视角下都能看得见,我们再让相机的位置适当偏移一下

camera.position.set(100, 150, 500);

然后,旋转和缩放的情况我们都看一下

咦这下我们似乎看出了一点端倪来,不管是通过旋转到达背面还是缩放到达背面,蓝线都是朝着屏幕向里,这时候用蓝线判断正反面似乎更为合理。

没错!我们抛开绕序,返璞归真,重新审视一下正反面,无非就是个朝向问题。而作为面片的法线——垂直于面片的蓝线正好与之相匹配。

我们再取个静止的图像看看

初始状态

旋转180度

scaleZ = -1

初始时,x轴正向到y轴正向是逆时针旋转,z轴向前

旋转180度时,x到y变成顺时针,z轴同时变成向后

scaleZ=-1时,x到y的旋转方向不变,但z轴向后了

这么一看,旋转似乎是负负得正的结果,而z方向缩放-1的操作,则只有一个负向的变换。

事实上,图2和图3的两个坐标轴是无法只通过旋转而完全重合。它就像我们的左右手,以及基于左右手定义的坐标系一样,是镜像关系。

顺带说句废话:如果您是名学霸或者读的专业跟化学有关系的话,您大概还会知道对映异构体这个词。它其实也是这种镜像关系,不同绕序化学性质也不一样,因此有的药名会带着左旋右旋这样的字眼。希望这个例子能帮助一部分小伙伴理解镜像变换。

笔者直接从百度百科偷了个图过来,让大家更形象的理解两套坐标系跟我们的左右手一样无法完全重合,总有一个方向是反的。

讲了这么多,我们对3D绕序的定义是时候跟2D区分开来了。它应该基于3个轴,如果定义右手坐标系为正绕序,那左手坐标系就是负绕序。

three.js使用的是右手坐标系,大家比划一下就可以发现,旋转180度后,坐标系仍为右手,而缩放-1的则把坐标系变成右手了。

因此对于3D版本的正负定矩阵,我们有这样的一个性质。

如果一个矩阵M可以把一个3维坐标系从左手坐标系变成右手坐标系,或者反过来,则矩阵M为负定矩阵,无更改则为正定矩阵,变换到共线共点就是零定矩阵。

下面我们就来推导一下,更改坐标系绕序(左右手方向)的矩阵是不是也正好对应上行列式的值。

初始时,x轴向量为(1,0,0),y轴向量为(0,1,0),z轴向量是(0,0,1),如果矩阵是负定矩阵,那么不管你如何去旋转它尝试让它跟变换前重合,就会发现总会有一个轴会变反的,这里我们取z为变反的轴。但是我们不能直接说它就是(0, 0, -1),因为可能会被拉长,缩短或者扭曲。但不管怎样,这个轴变换后的结果一定是跟初始方向不一致,也就是夹角大于90度。

这里想搞得通用点,也可以用任意的3个不共面向量来做推导,但是看上篇的2D版的式子都比较繁琐了,这里就还是不要折磨大家。

先来说步骤:

1 给定初始向量x(1,0,0),y(0,1,0),z(0,0,1),对xy做3D向量的叉乘(3D叉乘的结果为同时垂直于x和y的一个向量)计算,结果为(0,0,1),跟初始z的方向一致,也就是说,初始状态的坐标轴绕序为正(演算过程从略)

2 给定矩阵M,对初始点o(0,0,0),初始点x(1,0,0),初始点(0,1,0),初始点(0,0,1)做变换,得到新点o',x',y',z',同时算出向量o'x',o'y',o'z'

3 对o'x',o'y'做叉乘运算,得到向量V,判断它跟o'z'的夹角是否大于90度,可通过向量点乘运算求得,大于0为小于90度,小于0则相反

4 点乘结果的正负即为矩阵缩放的正负

下面就让我们开始吧!

4*4矩阵对点的变换计算如下所示(按照前面2D版的经验,本次直接给最后一行填充单位矩阵的数值)

3D向量点乘和叉乘的公式也在此处给出

点乘:

叉乘:

下面我们分别计算矩阵对4个点变换的结果。

可以看到,这个变换跟2D的真的很像,都是最后一列完全一样,所以在计算向量的过程中,最后一列会被完全消去。

得到的向量为

嗯,果然用给定的特殊坐标算出来的值简单很多。有兴趣的小伙伴可以自行捣鼓一般坐标的演算,笔者就不发上来折磨大家了。

下面我们算o'x'和o'y'的叉乘值,公式前面已给出,这里我们直接套用。

然后再跟o'z'做点乘运算

这两步计算连一起可称为向量的混合积

然后你会很惊喜地发现,向量混合积的结果跟4*4矩阵中前3阶的行列式一点不差!

加的部分

减的部分

同样地,three.js也严格按照4阶行列式定义书写Matrix4的determinant,跟2D版本一样,在最后一行用单位矩阵填充时,4*4的结果跟3*3是没有区别的。

然而4*4直接按定义写,跟3*3相比,多项式的项数会从6个飙升到24个。对性能来说并不友好。

所以我们看到three.js给的实现和注释给出的链接的写法是不太一样的。

该实现用了行列式的余子式写法(不懂的可以自行百度)把4*4行列式化为4个3阶,并且很巧妙地取到最后一行和一列作为剔除因子。因为在考察3D绕序,不研究透视w因子的情况下,最后一列一定是单位矩阵的数值,0,0,0,1,懂得编译原理的朋友应该就会明白,n41,n42和n43这3部分会因为第一个数为0而会对后面的运算过程做优化(当然脚本语言未必优化得彻底哈)。所以这一写法可以体现出three.js开发团队的功力确实够深的,这个细节优化也做到位了。

而3*3矩阵的行列式,就没有做这样的优化,大概是这里还没遇到瓶颈吧,换写法未必合适。

当然这里让笔者有点困惑,求逆的时候它也拿了余子式写法,并且取的不是最后一行最后一列,就有点谜,是怎么样顺手就怎么样写么?欢迎大佬们留言讨论。

写了这么多,结论是证明出来了,用左右手坐标系的方式定义3D绕序后,绕序,正负缩放,正负定矩阵是完全等价的。

回过头来说说2D,2D的叉乘是个阉割版,因为运算过程中通常不想扩展到3D,所以结果是一个数而非一个向量。但严格来说,2D向量叉乘的结果,它等于跟z轴平行的向量,长度等于2D叉乘的数值。如果给2D坐标全部加上z坐标,赋值为0,大家就会发现,其结论跟3D版完全一致。感兴趣的读者可自行演算。

因此,2D版的正负定矩阵是3D版的一个特例,它们的原理完全一致。

下面来小结一下:

1 3D中的点绕序需要结合3轴,用跟左右手坐标系类似的方式进行定义

2 绕序有没变更可通过两向量叉乘后跟第三向量点乘求得,也就是混合积

3 混合积结果跟4*4矩阵前3阶行列式一致

4 矩阵的正负缩放完全跟3*3矩阵的秩(或者最后一行填充了单位矩阵的4*4的秩)的符号直接对应

5 3D正负定矩阵的性质:正定矩阵不修改坐标系的左右手方向(绕序),负定矩阵会修改,零定矩阵会把坐标系压缩为共面,共线或者共点。零定矩阵不可逆。

好了,折腾完这波理论,下篇就暂时不折磨大家了,换回踩坑经验方面的分享。感谢小伙伴们的支持和关注,我们下篇再见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/10869.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

反向代理模块jmh

1 概念 1.1 反向代理概念 反向代理是指以代理服务器来接收客户端的请求&#xff0c;然后将请求转发给内部网络上的服务器&#xff0c;将从服务器上得到的结果返回给客户端&#xff0c;此时代理服务器对外表现为一个反向代理服务器。 对于客户端来说&#xff0c;反向代理就相当…

软件工程经济学-日常作业+大作业

目录 一、作业1 作业内容 解答 二、作业2 作业内容 解答 三、作业3 作业内容 解答 四、大作业 作业内容 解答 1.建立层次结构模型 (1)目标层 (2)准则层 (3)方案层 2.构造判断矩阵 (1)准则层判断矩阵 (2)方案层判断矩阵 3.层次单排序及其一致性检验 代码 …

【回溯】目标和 字母大小全排列

文章目录 494. 目标和解题思路&#xff1a;回溯784. 字母大小写全排列解题思路&#xff1a;回溯 494. 目标和 494. 目标和 给你一个非负整数数组 nums 和一个整数 target 。 向数组中的每个整数前添加 或 - &#xff0c;然后串联起所有整数&#xff0c;可以构造一个 表达式…

告别复杂,拥抱简洁:用plusDays(7)代替plus(7, ChronoUnit.DAYS)

前言 你知道吗?有时候代码里的一些小细节看起来很简单,却可能成为你调试时的大麻烦。在 Java 中,我们用 LocalDateTime 进行日期和时间的操作时,发现一个小小的替代方法可以让代码更简洁,功能更强大。这不,今天我们就来探讨如何用 LocalDateTime.now().plusDays(7) 替代…

《苍穹外卖》项目学习记录-Day10订单状态定时处理

利用Cron表达式生成器生成Cron表达式 1.处理超时订单 查询订单表把超时的订单查询出来&#xff0c;也就是订单的状态为待付款&#xff0c;下单的时间已经超过了15分钟。 //select * from orders where status ? and order_time < (当前时间 - 15分钟) 遍历集合把数据库…

【深度分析】微软全球裁员计划不影响印度地区,将继续增加当地就业机会

当微软的裁员刀锋掠过全球办公室时&#xff0c;班加罗尔的键盘声却愈发密集——这场资本迁徙背后&#xff0c;藏着数字殖民时代最锋利的生存法则。 表面是跨国公司的区域战略调整&#xff0c;实则是全球人才市场的地壳运动。微软一边在硅谷裁撤年薪20万美金的高级工程师&#x…

Linux中 端口被占用如何解决

lsof命令查找 查找被占用端口 lsof -i :端口号 #示例 lsof -i :8080 lsof -i :3306 netstat命令查找 查找被占用端口 netstat -tuln | grep 端口号 #示例 netstat -tuln | grep 3306 netstat -tuln | grep 6379 ss命令查找 查找被占用端口 ss -tunlp | grep 端口号 #示例…

qt-Quick3D笔记之官方例程Runtimeloader Example运行笔记

qt-Quick3D笔记之官方例程Runtimeloader Example运行笔记 文章目录 qt-Quick3D笔记之官方例程Runtimeloader Example运行笔记1.例程运行效果2.例程缩略图3.项目文件列表4.main.qml5.main.cpp6.CMakeLists.txt 1.例程运行效果 运行该项目需要自己准备一个模型文件 2.例程缩略图…

高性能消息队列Disruptor

定义一个事件模型 之后创建一个java类来使用这个数据模型。 /* <h1>事件模型工程类&#xff0c;用于生产事件消息</h1> */ no usages public class EventMessageFactory implements EventFactory<EventMessage> { Overridepublic EventMessage newInstance(…

Spring Boot项目如何使用MyBatis实现分页查询

写在前面&#xff1a;大家好&#xff01;我是晴空๓。如果博客中有不足或者的错误的地方欢迎在评论区或者私信我指正&#xff0c;感谢大家的不吝赐教。我的唯一博客更新地址是&#xff1a;https://ac-fun.blog.csdn.net/。非常感谢大家的支持。一起加油&#xff0c;冲鸭&#x…

【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】1.27 线性代数王国:矩阵分解实战指南

1.27 线性代数王国&#xff1a;矩阵分解实战指南 #mermaid-svg-JWrp2JAP9qkdS2A7 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-JWrp2JAP9qkdS2A7 .error-icon{fill:#552222;}#mermaid-svg-JWrp2JAP9qkdS2A7 .erro…

EasyExcel使用详解

文章目录 EasyExcel使用详解一、引言二、环境准备与基础配置1、添加依赖2、定义实体类 三、Excel 读取详解1、基础读取2、自定义监听器3、多 Sheet 处理 四、Excel 写入详解1、基础写入2、动态列与复杂表头3、样式与模板填充 五、总结 EasyExcel使用详解 一、引言 EasyExcel 是…

FIDL:Flutter与原生通讯的新姿势,不局限于基础数据类型

void initUser(User user); } 2、执行命令./gradlew assembleDebug&#xff0c;生成IUserServiceStub类和fidl.json文件 3、打开通道&#xff0c;向Flutter公开方法 FidlChannel.openChannel(getFlutterEngine().getDartExecutor(), new IUserServiceStub() { Override void…

DIFY源码解析

偶然发现Github上某位大佬开源的DIFY源码注释和解析&#xff0c;目前还处于陆续不断更新地更新过程中&#xff0c;为大佬的专业和开源贡献精神点赞。先收藏链接&#xff0c;后续慢慢学习。 相关链接如下&#xff1a; DIFY源码解析

87.(3)攻防世界 web simple_php

之前做过&#xff0c;回顾 12&#xff0c;攻防世界simple_php-CSDN博客 进入靶场 <?php // 显示当前 PHP 文件的源代码&#xff0c;方便调试或查看代码结构 // __FILE__ 是 PHP 的一个魔术常量&#xff0c;代表当前文件的完整路径和文件名 show_source(__FILE__);// 包含…

x86-64数据传输指令

关于汇编语言一些基础概念的更详细的介绍&#xff0c;可移步MIPS指令集&#xff08;一&#xff09;基本操作_mips指令 sw-CSDN博客 该指令集中一个字2字节。 该架构有16个64位寄存器&#xff0c;名字都以%r开头&#xff0c;每个寄存器的最低位字节&#xff0c;低1~2位字节&…

网络工程师 (8)存储管理

一、页式存储基本原理 &#xff08;一&#xff09;内存划分 页式存储首先将内存物理空间划分成大小相等的存储块&#xff0c;这些块通常被称为“页帧”或“物理页”。每个页帧的大小是固定的&#xff0c;例如常见的页帧大小有4KB、8KB等&#xff0c;这个大小由操作系统决定。同…

全程Kali linux---CTFshow misc入门(25-37)

第二十五题&#xff1a; 提示&#xff1a;flag在图片下面。 直接检查CRC&#xff0c;检测到错误&#xff0c;就直接暴力破解。 暴力破解CRC的python代码。 import binascii import struct def brute_force_ihdr_crc(filename): # 读取文件二进制数据 with open(filen…

MySQL数据库(二)- SQL

目录 ​编辑 一 DDL (一 数据库操作 1 查询-数据库&#xff08;所有/当前&#xff09; 2 创建-数据库 3 删除-数据库 4 使用-数据库 (二 表操作 1 创建-表结构 2 查询-所有表结构名称 3 查询-表结构内容 4 查询-建表语句 5 添加-字段名数据类型 6 修改-字段数据类…

Android记事本App设计开发项目实战教程2025最新版Android Studio

平时上课录了个视频&#xff0c;从新建工程到打包Apk&#xff0c;从头做到尾&#xff0c;没有遗漏任何实现细节&#xff0c;欢迎学过Android基础的同学参加&#xff0c;如果你做过其他终端软件开发&#xff0c;也可以学习&#xff0c;快速上手Android基础开发。 Android记事本课…