DirectX11--HLSL中矩阵的内存布局和mul函数探讨
前言
说实话,我感觉这是一个大坑,不知道为什么要设计成这样混乱的形式。
(2020年4月12日更新)过了几年回头看这篇博客,连我自己都看的头发都乱了,又重新理了一遍才把自己说通。。于是这篇博客又被我大改了一遍。
在我用的时候,以row_major
矩阵,并且mul函数以向量左乘矩阵的形式来绘制时的确能够正常显示,并不会有什么感觉。但是也有人会遇到明明传的矩阵没有问题,却怎么样都绘制不出的情况;或者使用一遍矩阵,在mul函数用向量左乘的形式却又可以绘制出来的疑问。因此本文目的就是要扫清这些障碍。
ps. 本问题由淡一抹夕霞提供。
DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。
一些线性代数基础
行主序矩阵与列主序矩阵
首先要了解的是,对于连续内存数据:
\]
行主序矩阵是这样解释数据的:
\]
而列主序矩阵是这样解释数据的:
\]
显然,行主序矩阵经过一次转置后就会变成列主序矩阵
矩阵左乘与右乘
由于矩阵乘法不满足交换律,则需要区分当前矩阵位于乘号的左边还是右边。有时候经常都会听到左乘和右乘这两个概念,下面是有关它们的含义:
左乘指的是该矩阵位于乘号的左边,例如:行向量 左乘 矩阵,即行向量在乘号的左边
右乘指的是该矩阵位于乘号的右边,例如:列向量 右乘 矩阵,即列向量在乘号的右边
ps. 向量也是矩阵
行向量v
和矩阵M
满足下面的关系:
\]
C++和HLSL中矩阵的内存布局
在C++的DirectXMath中,无论是XMFLOAT4X4
,还是使用函数生成的XMMATRIX
,都是采用行主序矩阵的解释方式。它的数据流如下:
\]
上述数据流传递到HLSL后,若是传递给cb0的寄存器的前4个向量,那么它内存布局一定如下:
cb0[0].xyzw = (m11, m12, m13, m14);
cb0[1].xyzw = (m21, m22, m23, m24);
cb0[2].xyzw = (m31, m32, m33, m34);
cb0[3].xyzw = (m41, m42, m43, m44);
而在HLSL中,默认的matrix
或float4x4
采用的是列主序矩阵的解释形式。
假设在HLSL的cbuffer
为:
cbuffer cb : register(b0)
{
(row_major) matrix g_World;
}
如果g_World
是matrix
或float4x4
类型,由于是列主序矩阵,上面的4个寄存器存储的数据会被看作:
m_{11} & m_{21} & m_{31} & m_{41} \\
m_{12} & m_{22} & m_{32} & m_{42} \\
m_{13} & m_{23} & m_{33} & m_{43} \\
m_{14} & m_{24} & m_{34} & m_{44} \\
\end{bmatrix}
\]
而如果g_World
是row_major matrix
或row_major float4x4
类型,则为行主序矩阵,上面的4个寄存器存储的数据则依然被视作:
\]
HLSL中的mul函数
微软的官方文档是这么描述mul函数的(微软官方文档链接),这里进行个人翻译:
使用矩阵数学来进行矩阵x左乘矩阵y的运算,要求矩阵x的列数与矩阵y的行数相等。
如果x是一个向量,那么它将被解释为行向量。
如果y是一个向量,那么它将被解释为列向量。
表面上看起来很美满,很智能,但稍有不慎就要在这里踩大坑了。
dp4指令
dp4是一个汇编指令(微软官方文档链接),使用方法如下:
dp4 dst, src0, src1
其中 src0和src1是一个向量,计算它们的点乘并将结果传给dst。
当然这里并不是要教大家怎么写汇编,而是怎么看。
为了了解mul
函数是如何进行向量与矩阵的乘法运算,我们需要探讨一下它的汇编实现。这里我所使用的是row_major
矩阵。首先是向量作为第一个参数的情况:
可以看到这种运算方式实际上却是按照向量右乘矩阵的形式进行的运算。
然后是将向量作为第二个参数的情况(仅单纯的参数交换):
无论是行向量左乘矩阵,还是列向量右乘矩阵,在汇编层面上都是用dp4
的形式进行计算,这是因为对矩阵来说在内存上是以4个行向量的形式存储的,传递一行的寄存器向量比传递一列更简单,适合进行与列向量的运算,并且效率会更高。
然后眼尖的同学会发现,同一条指令,只是改变了顺序,指令执行的起始地址就产生了差别(16条指令数目差)。
但是交换两个参数又会导致运算结果/显示结果的不同,这时候就要看看矩阵所存的值了。
先看一段HLSL代码:
cbuffer cb : register(b0)
{
row_major matrix gWorld;
row_major matrix gView;
row_major matrix gProj;
}
struct VertexPosNormalTex
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
};
struct VertexPosHWNormalTex
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION; // 在世界中的位置
float3 NormalW : NORMAL; // 法向量在世界中的方向
float2 Tex : TEXCOORD;
};
// 顶点着色器
VertexPosHWNormalTex VS(VertexPosNormalTex pIn)
{
VertexPosHWNormalTex pOut;
row_major matrix viewProj = mul(gView, gProj);
pOut.PosW = mul(float4(pIn.PosL, 1.0f), gWorld).xyz;
pOut.PosH = mul(float4(pOut.PosW, 1.0f), viewProj);
pOut.NormalW = mul(pIn.NormalL, (float3x3) gWorldInvTranspose);
pOut.Tex = pIn.Tex;
return pOut;
}
我们只考虑viewProj
的初始化和pOut.PosH
的赋值操作。
首先是viewProj
经过计算后应该得到的值:
这是向量左乘矩阵开始前四个向量寄存器的值(默认HLSL):
这是向量右乘矩阵开始前时四个向量寄存器的值(将float4(pOut.PosW, 1.0f)
和viewProj
交换):
也许有人会奇怪,怎么在开始运算前两边寄存器存储的内容会不一样。我们需要往前观察上一个语句产生的汇编(默认HLSL):
而将float4(pOut.PosW, 1.0f)
和viewProj
交换后,则汇编代码没有了转置操作:
严格意义上说,00000000到0000001B的指令才是上图语句的实际执行内容,而0000001C到0000002B的代码则应是在计算pOut.PosH = mul(float4(pOut.PosW, 1.0f), viewProj);
之前所进行的一系列额外操作。
因此无论是行向量还是列向量,在执行完0000001B指令后,行主序矩阵viewProj
的内存布局一定为:
如果用行向量左乘该行主序矩阵,由于dp4运算需要按列取出这些寄存器的值,为此需要额外16条指令进行转置(0000001C到0000002B)。
而如果用列向量右乘该行主序矩阵,则不需要进行转置,直接取寄存器行向量就可以直接进行dp4运算。
因此,我们可以知道一个行向量左乘行主序矩阵时,为了满足mul
函数使用dp4
指令优化运算,需要会预先对原来的矩阵进行转置。其中r4 r5 r6 r3为viewProj
转置后的矩阵,即将会左乘向量float4(pOut.PosW, 1.0f)
。而列向量右乘行主序矩阵则可以避免转置操作。
同理,如果采用列主序矩阵,行向量左乘列主序矩阵可以避免转置操作;而列向量右乘行主序矩阵又会产生转置操作。
故对于dp4
来说,最好是能够对一个行向量和列主序矩阵(取列主序矩阵的行,也就是取一行寄存器向量与行向量做点乘)操作,又或者是对一个行主序矩阵(取行主序矩阵的行与列向量做点乘)和列矩阵操作,这样都能有效避免转置。
总结
综上所述,有三处地方可能会发生转置:
- C++代码端的转置
- HLSL中matrix(float4x4)是列主矩阵时会发生转置
- mul乘法内部是以列向量右乘矩阵的形式实现的,对于行向量左乘矩阵的情况会发生转置
经过组合,就一共有四种能够正常绘制的情况:
- C++代码端不进行转置,HLSL中使用
row_major matrix
(行主序矩阵),mul函数让向量放在左边(行向量),这样实际运算就是(行向量 X 行主序矩阵) 。这种方法易于理解,但是这样做dp4运算取矩阵的列很不方便,在HLSL中会产生用于转置矩阵的大量指令,性能上有损失。 - C++代码端进行转置,HLSL中使用
matrix
(列主序矩阵) ,mul函数让向量放在左边(行向量),这样就是(行向量 X 列主序矩阵),但C++这边需要进行一次矩阵转置,HLSL内部不产生转置 。这是官方例程所使用的方式,这样可以使得dp4运算可以直接取列主序矩阵的行,从而避免内部产生大量的转置指令。后续我会将教程的项目也使用这种方式。 - C++代码端不进行转置,HLSL中使用
matrix
(列主序矩阵),mul函数让向量放在右边(列向量),实际运算是(列主序矩阵 X 列向量)。这种方法的确可行,取列矩阵的行也比较方便,效率上又和2等同,就是HLSL那边的矩阵乘法都要反过来写,然而DX本身就是崇尚行主矩阵的,把OpenGL的习惯带来这边有点。。。 - C++代码端进行转置,HLSL中使用
row_major matrix
(行主序矩阵),mul函数让向量放在右边(列向量),实际运算是(行主序矩阵 X 列向量)。 就算这种方法也可以绘制出来,但还是很让人难受,比第2点还难受,我甚至不想去说它。
也就是说,以组合1为基准,任意改变其中两个状态都不会影响最终结果。
DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。
DirectX11--HLSL中矩阵的内存布局和mul函数探讨的更多相关文章
- JVM中对象的内存布局与访问定位
一.对象的内存布局 已主流的HotSpot虚拟机来说, 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header).实例数据(Instance Data)和对齐填 ...
- STL库中string类内存布局的探究
在STL中有着一个类就是string类,他的内存布局和存储机制究竟是怎么样的呢? 这就是建立好的string 可以看出,图中用黄色框框标注的部分就是主要区域 我们用来给string对象进行初始化的字符 ...
- 99.9%的Java程序员都说不清的问题:JVM中的对象内存布局?
本文转载自公众号:石彬的架构笔记,阅读大约需要8分钟. 作者:李瑞杰 目前就职于阿里巴巴,资深 JVM 研究人员 在 Java 程序中,我们拥有多种新建对象的方式.除了最为常见的 new 语句之外,我 ...
- 理解numpy中ndarray的内存布局和设计哲学
目录 ndarray是什么 ndarray的设计哲学 ndarray的内存布局 为什么可以这样设计 小结 参考 博客:博客园 | CSDN | blog 本文的主要目的在于理解numpy.ndarra ...
- C# Struct的内存布局
转载:http://www.csharpwin.com/csharpspace/10454r4891.shtml 问题:请说出以下struct的实例大小以及内存布局 struct Struct1 { ...
- vs查看虚函数表和类内存布局
虚继承和虚基类 虚继承:在继承定义中包含了virtual关键字的继承关系: 虚基类:在虚继承体系中的通过virtual继承而来的基类,需要注意的是:class CSubClass : publ ...
- C++对象的内存布局以及虚函数表和虚基表
C++对象的内存布局以及虚函数表和虚基表 本文为整理文章, 参考: http://blog.csdn.net/haoel/article/details/3081328 http://blog.csd ...
- VS2013命令行界面查看虚函数的内存布局
内存布局可能使用vs的界面调试看到的旺旺是一串数字,很不方便,但是vs的命令行界面可以很直观的显示出一个类中具体的内存布局. 打开命令行.界面如下所示: 测试代码如下所示: class Base1 { ...
- 【JVM之内存与垃圾回收篇】对象实例化内存布局与访问定位
对象实例化内存布局与访问定位 从各自具体的内存分配上来讲 new 的对象放在堆中 对象所属的类型信息是放在方法区的 方法当中的局部变量放在栈空间 这 new 的对象怎么把三块粘合到一起 就是这章的内容 ...
随机推荐
- 【Python实战】使用Python连接Teradata数据库???未完成
1.安装Python 方法详见:[Python 05]Python开发环境搭建 2.安装Teradata客户端ODBC驱动 安装包地址:TTU下载地址 (1)安装TeraGSS和tdicu(ODBC依 ...
- HBase实践案例:知乎 AI 用户模型服务性能优化实践
用户模型简介 知乎 AI 用户模型服务于知乎两亿多用户,主要为首页.推荐.广告.知识服务.想法.关注页等业务场景提供数据和服务, 例如首页个性化 Feed 的召回和排序.相关回答等用到的用户长期兴趣特 ...
- audio元素和video元素在ios和andriod中无法自动播放
原因: 因为各大浏览器都为了节省流量,做出了优化,在用户没有行为动作时(交互)不予许自动播放: /音频,写法一 <audio src="music/bg.mp3" autop ...
- 距离放弃python又近了一大步,而然只是第四天
今天是周末后的第一天,周末四处浪浪浪,所以在周一的时候就要狠狠的复习之前的东西了,之后从第一天的计算机基础开始复习,具体内容请翻阅前三篇随笔,主要是要仔细看看,怕学了后面的忘了前面的,今天引进的第一个 ...
- Spring Boot:The field file exceeds its maximum permitted size of 1048576 bytes
错误信息:The field file exceeds its maximum permitted size of 1048576 bytes原因是因为SpringBoot内嵌tomcat默认所能上传 ...
- Mango 基础知识
1 mongdb和python交互的模块 pymongo 提供了mongdb和python交互的所有方法 安装方式: pip install pymongo 2 使用pymongo 1. 导入pymo ...
- iOS开发基础-KVC简单介绍
一.KVC介绍 键值编码(Key Value Coding,KVC):使开发者不必进行任何操作就可以进行属性的动态读写. KVC操作方法由 NSKeyValueCoding 协议提供,而 NSObje ...
- .Net Core应用框架Util介绍(一)
距离上次发文,已经过去了三年半,这几年技术更新节奏异常迅猛,.Net进入了跨平台时代,前端也被革命性的颠覆. 回顾 2015年,正当我还沉迷于JQuery + EasyUi的封装时,突然意识到技术已经 ...
- Oracle通过Navicat导入表数据与机构,数据无法直接查询,需要加双引号的问题
使用navicat 导入表到ORACLE时,总是会遇到虽然表格完整导入到数据库,但是往往查不出来数据,网上提供的解决办法是把查询的列 加上 双引号,或者表名加上双引号,但这解决办法却减慢了编写sql ...
- 区块链代币(Token)笔记 — — 术语
前言 接触区块链和数字货币差不多有大半年时间,一直在赶项目进度,现在有空整理补习一下相关的知识,只谈代币不谈区块链