Skip to content
是封面

对展示实体渲染变换的研究

徐木弦

徐木弦

引言

展示实体作为Minecraft的技术性实体之一,它们的作用主要体现在视觉方面。这些实体没有碰撞箱,没有任何自主行为,只能通过技术手段生成。在生成时如果不指定NBT,则不会显示任何内容。原版技术开发者可以用展示实体的常规字段展示一些普通的内容,如正常形状的方块、物品、文字,但如果仅用展示实体展示这些常规内容,未免有些单调。
展示实体的transformation字段是实体格式中较复杂的一个字段,它使用矩阵形式或分解形式来表示展示实体的渲染变换,从而制造一些特殊的效果。

矩阵形式

使用矩阵形式时,字段transformation的数据类型为列表,列表内一共有16个元素,这些元素均为单精度浮点数。这个列表用于表示一个4×4的行主序仿射变换矩阵。为了以矩阵形式表示三维空间中点的变换,将原空间映射至仿射空间,对于三维空间内每一个点(x0,y0,z0),在其尾部添加一个1以在仿射空间内表示一个点,即(x0,y0,z0,1)。令该点经过一定仿射变换A后位于(x,y,z,1),则写成矩阵乘法的形式:

[xyz1]=[a11a12a13a14a21a22a23a24a31a32a33a34a41a42a43a44][x0y0z01]

基础变换形式有平移、旋转、缩放(镜像)、剪切,所有的变换均基于实体的实际坐标进行。

平移

设展示实体上任意一点(x0,y0,z0,1)xyz轴分别平移abc后得到点(x,y,z,1),则

{x=x0+ay=y0+bz=z0+c1=1

则平移矩阵T

T(a,b,c)=[100a010b001c0001]

旋转

一共有三种旋转方式,即绕x轴、绕y轴和绕z轴旋转。以绕x轴旋转α为例,假设实体上一点A和实体锚点O所成直线与z轴的夹角为φ,令OA的模为l,则有

{x=xy=lcosφz=lsinφ

OAx轴旋转α得到OA,此时有

{x=xy=lcos(φ+α)=lcosφcosαlsinφsinαz=lsin(φ+α)=lsinφcosα+lcosφsinα

于是有

{x=xy=ycosαzsinαz=ysinα+zcosα

将其转换为仿射矩阵,得到

Rx(α)=[10000cosαsinα00sinαcosα00001]

同理,绕y轴旋转β的矩阵形式为

Ry(β)=[cosβ0sinβ00100sinβ0cosβ00001]

z轴旋转γ的矩阵形式为

Rz(γ)=[cosγsinγ00sinγcosγ0000100001]

缩放

设展示实体上任意一点(x0,y0,z0,1)沿xyz轴分别缩放mnp倍后得到点(x,y,z,1),则

{x=mx0y=ny0z=pz01=1

则缩放矩阵S

S(m,n,p)=[m0000n0000p00001]

m=n=p,则是均匀缩放;不然则是非均匀缩放。

镜像

对于缩放矩阵而言,特别地,若mnp三者中至少有一个为负数,都会进行镜像变换。负缩放因子使坐标系在对应轴上反转,表面法线方向改变,从而造成内凹渲染。
镜像变换造成的内凹渲染
若展示实体上任意一点(x0,y0,z0,1)沿x轴镜像,其他方向上不作变化,易得镜像矩阵

Mx(m)=[m000010000100001]

其中m<0。同理可得沿y轴镜像、沿z轴镜像的矩阵My(n)Mz(p)。在多个方向进行的镜像变换也很容易得出,例如在x轴、y轴、和z轴方向上同时应用镜像变换所需的矩阵(m<0n<0p<0)为

Mx,y,z(m,n,p)=[m0000n0000p00001]

剪切

剪切变换将实体上所有点沿某一方向做一定移动,通过原点的直线上任意一点沿该方向移动的距离随直线与原点的距离线性变化,这使得图像变得倾斜。一种剪切变换发生在两个正交坐标轴组成的平面内,在其中一个方向上做剪切,在另一个方向上不做变换。三维坐标系中坐标轴两两正交一共有六对正交关系,因此初等剪切变换一共有六种。
剪切变换
如图,图像在一个方向发生剪切的过程中,实际上与另一个方向拥有一个剪切角度θi,j,下标(i,j)代表在i方向内做剪切,并与j方向呈一定剪切角度。若图中横向为x轴,纵向为y轴,剪切角度记为θx,y,显然有

{x=x0+y0tanθx,yy=y0z=z01=1

x轴方向上做剪切、并与y轴方向呈一定剪切角度所需矩阵H

H(θx,y)=[1tanθx,y00010000100001]

同理可推导得到其他六种剪切变换所需的矩阵。当剪切变换的方向为x轴时,元素tanθi,j一定位于矩阵的第一行,y轴则为第二行,z轴则为第三行;与变换方向呈剪切角度的方向为x轴时,元素tanθi,j一定位于第一列,y轴则为第二列,z轴则为第三列。例如,某个剪切变换在z轴方向进行,与x轴方向呈剪切角度,则tanθz,x位于第三行第一列。
以上描述的剪切矩阵均为仅在一个方向上做变换,并与另一个方向呈一定剪切角度的情况。若同时应用多个不同的剪切变换,使用上面的规律填入元素,则剪切矩阵可记为

H(θx,y,θx,z,θy,x,θy,z,θz,x,θz,y)=[1tanθx,ytanθx,z0tanθy,x1tanθy,z0tanθz,xtanθz,y100001]

若某个方向的剪切变换不使用,将矩阵中对应位置的tanθi,j写为0即可。

组合变换

一种变换可能无法满足要求,有时需要同时应用多种以表示复杂的变换。对于有限个仿射变换A1A2、……An,依次将它们作用于一点x,则变换后得到的点x

x=AnAn1A2A1x

注意矩阵的乘法遵循从右向左的运算规则,且不支持交换律,但是支持结合律,因此有

x=(AnAn1A2A1)x

A=AnAn1A2A1,则x=Ax,其中A为组合变换矩阵。组合变换中各种变换的次序非常重要,上一个变换可能会影响下一个变换的结果。
在标签transformation中使用的矩阵均为组合变换矩阵。

应用实例

修改一个方块展示实体的NBT数据,使之依次绕y轴旋转30、绕x轴旋转45、绕z轴旋转90。 求出组合变换矩阵,注意按从右向左的顺序计算:

A=Rz(90)Rx(45)Ry(30)=[cos90sin9000sin90cos900000100001][10000cos45sin4500sin45cos4500001][cos300sin3000100sin300cos3000001]=[242264032012024226400001][0.350.710.6100.8700.500.350.710.6100001]

故命令应为

mcfunction
data merge entity @e[type=block_display,limit=1] {transformation:[-0.35f,-0.71f,0.61f,0.0f,0.87f,0.0f,0.5f,0.0f,-0.35f,0.71f,0.61f,0.0f,0.0f,0.0f,0.0f,1.0f]}

分解形式

对于这些4×4大小的仿射变换矩阵A,其元素a41a42a43总是为0,a44总是为1,若不为1,则将整个矩阵按1a44的比例缩放,从而使a44为1。可以将其分块写成如下的形式:

A=[a11a12a13a14a21a22a23a24a31a32a33a34a41a42a43a44]=[B3×3T3×1O1×3E1×1]

式中分块阵B是左上角3×3区域,这个区域代表模型的线性变换,存储了包括旋转、缩放、镜像和剪切在内的所有线性变换数据,注意,这个分块阵不适用于平移变换,因为平移变换不是线性变换。而分块阵T的三个元素仅被平移变换所使用。
分解形式的transformation字段是分块阵B经奇异值分解后使用的数据。对于任意的3阶方阵B,总存在3阶正交方阵UV、3阶对角阵Σ,有

B=UΣVT

式中:
VT——矩阵V的转置矩阵。
U为左奇异向量矩阵,V为右奇异向量矩阵,对角阵Σ中对角线上的三个元素被称为奇异值。下面介绍奇异值分解的计算方法。
对上式等号左右两边取转置矩阵,得

BT=VΣUT

由于方阵UV是正交的,因此VTV=EUTU=E。则有

BBT=UΣVTVΣUT=UΣ2UT

对上式进行变形:

UT(BBT)U=Σ2

方阵BBT是一个实对称阵,显然上式描述的是将BBT相似对角化的过程,其中Σ=[σ1σ2σ3],使用的正交阵便为左奇异向量矩阵U。如果记λ1λ2λ3BBT的三个特征值,这些特征值是非负的,读者可自行证明,于是有

Σ2=[λ1λ2λ3]=[σ12σ22σ32]

求出BBT的三个特征值即可求出对角阵Σ。因此,ΣU的求解步骤如下——
步骤一:
由特征方程|λEBBT|=0BBT的全部特征值λi,然后求出对角阵Σ=diag(σ1,σ2,σ3)=diag(λ1,λ2,λ3)
步骤二:
对于每个特征值λi,由方程组(λiEBBT)x=0求对应的特征向量αi
步骤三:
如果求得的特征向量相互不正交,则对特征向量αi进行正交化,记正交化后的向量为βi
步骤四:
如果求得的向量βi没有单位化,则将其单位化为γi,令U=[γ1,γ2,γ3]。计算完毕。
对于右奇异向量矩阵V,有

BTB=VΣUTUΣVT=VΣ2VT

同理可求得右奇异向量矩阵,计算步骤与上述计算左奇异向量矩阵的步骤相同,其中Σ和上文是同一个矩阵,可不必重复计算。若B可逆,则

V=B1UΣ

这样可以不进行对角化计算而直接求出右奇异向量矩阵V
矩阵奇异值分解的结果具有几何意义,其中UV是旋转变换矩阵,Σ是缩放变换矩阵。任何变换都可以被分解成四个过程:初次旋转变换、缩放变换、再次旋转变换和平移变换。因此,用V表示初次旋转变换,用Σ表示缩放变换,用U表示再次旋转变换,在此基础上再引入平移向量T,则可以得到变换矩阵A的分解形式,此时字段transformation是复合标签:

transformation:根标签 └─right_rotation:模型进行缩放变换前的旋转变换,即初次旋转变换, 与奇异值分解中的V相关。拥有两种可用数据形式:轴角式和四元数形式。 编写时可以使用轴角式,但是在存储数据时一律转换成四元数形式。 └─ (初次旋转数据) └─scale:模型的缩放变换,与奇异值分解中的∑相关。使用三维向量。 └─ (向量的一个分量) └─left_rotation:模型进行缩放变换后的旋转变换,即再次旋转变换, 与奇异值分解中的U相关。同样有轴角式和用四元数形式两种表示方式。 编写时可以使用轴角式,但是在存储数据时一律转换成四元数形式。 └─ (再次旋转数据) └─translation:模型的平移变换 T。 对应矩阵形式最后一列前三行元素。使用三维向量。 └─ (一个分量)

对于right_rotationleft_rotation这两个字段,有轴角式和四元数形式两种数据形式表示旋转。下面分别介绍这两种数据形式:

轴角式

轴角式旋转可以理解为:一个向量v绕一个通过原点(即实体实际位置)的长度为1的轴u旋转角度θ得到向量v。此时有u=1
轴角式旋转示意图
为了便于分析,将向量v分解成平行于轴u的向量v和正交于轴u的向量v,于是有

v=v+v

向量v的分解
v用含有vu的式子表达,即计算vu上的投影:

v=vuu=(uv)uuu=(uv)u

于是可得到v的表达式

v=vv=v(uv)u

对于向量v,同样可以将其分解得到

v=v+v

实际上,在向量v的旋转过程中,向量v没有发生变化,即

v=v

向量v⊥的旋转
现在考察向量v的旋转。不难发现,向量的旋转实际上是发生在圆周上的。此时正交于u轴的平面内没有其他可用轴,为此构建同时正交于uv的轴w,有

w=u×v

w=u×v=uvsin90=v

wv的模是相等的,故将向量v可被分解为平行于wvw和平行于vvv,有

v=vw+vv=wsinθ+vcosθ=(u×v)sinθ+vcosθ

所以得到

v=v+v=v+(u×v)sinθ+vcosθ=v+[u×(vv)]sinθ+vcosθ=v+(u×v)sinθ+vcosθ=(uv)u+(u×v)sinθ+[vv=v(uv)u]cosθ=(uv)u(1cosθ)+(u×v)sinθ+vcosθ

使用轴角式表示旋转时字段right_rotationleft_rotation为复合标签:

left_rotationright_rotation├─angle:绕轴旋转的角度,即 θ 角,采用角度制。 └─axis:含三个元素的有序数组,用于定义旋转轴向量 uu。一般可以写成单位向量。 └─ (向量的一个分量)

四元数形式

使用四元数形式表示旋转时,字段right_rotationleft_rotation类型是列表,数据格式为:

left_rotationright_rotation:表示四元数的四个元素,顺序依次为 x、y、z、w。 └─ (四元数中的一个元素)

一切四元数都可以写成如下的形式:

q=w+xi+yj+zk

其中wxyzR,称xi+yj+zk为四元数q的虚部,w为实部。一般可以使用向量q=(w,x,y,z)来表示四元数,或者将(x,y,z)视作一个向量v,用标量和向量的形式表示四元数q=(w,v)。四元数的模为q=w2+x2+y2+z2,规定:当q=1时,该四元数为单位四元数。同时又有规定:当w=0时,可以称该四元数为纯四元数。
对于轴角式中的旋转轴和向量,可以将其写成纯四元数的形式,如u=(0,u)v=(0,v)。因此有:

v=v+vv=v+v

v的旋转可表示为

v=v

如果将(usinθ+cosθ)视作一个四元数q,即q=(cosθ,usinθ),则可得

v=qv

注意到,上面的这个四元数q有如下性质:

q=cos2θ+usinθusinθ=cos2θ+u2sin2θ=1

这是一个单位四元数。一般应用于旋转变换的四元数都是单位四元数,\emphasize{非单位四元数会使得模型在旋转的同时进行缩放}。于是使用四元数形式表示的向量旋转为

v=v+v=v+qv

q=p2,其中p=(cosθ2,usinθ2),则

v=v+qv=ppv+p2v=pvp+pvp=p(v+v)p=pvp

式中: p——四元数p的共轭,若p=(w,v),则p=(w,v)
于是得到了四元数形式表示的旋转公式:

v=qvq

其中q=(cosθ2,usinθ2)。这个四元数中各元素分别为w=cosθ2x=uxsinθ2y=uysinθ2z=uzsinθ2
式中:
θ——绕轴u旋转的角度,方向为逆时针。
ui——旋转轴u在该坐标轴i上的分量。
对于一个渲染变换,设其初次旋转所用四元数为qr,再次旋转所用四元数为ql,令缩放数据s=(sx,sy,sz),平移数据t=(tx,ty,tz)。对展示实体上任意一点A(x0,y0,z0)构造四元数

q0=x0i+y0j+z0k=(0,OA)

进行初次旋转,得到

q1=qrq0qr

随后应用缩放变换,得到

q2=sxq1xi+syq1yj+szq1zk

在初次旋转和放缩变换共同作用下,模型中各点的相对位置会发生改变。只有当初次旋转四元数qr=(1,0)(不发生旋转)或缩放数据s=(1,1,1)(不进行缩放)时,模型才不会发生变形。模型在这之后会根据再次旋转变换确定最终的旋转角度,得到

q3=qlq2ql

最后应用平移变换,确定模型最终的位置,从而得到点A最终的位置:

q=q3+t

应用实例

用方块展示实体展示一个玻璃。要求:生成这个展示实体,使玻璃的体对角线与y轴平行。使这个展示实体绕体对角线旋转,旋转一周用时4秒。
模型中体对角线从O(0,0,0)A(1,1,1),现在需要使模型在不发生形变的前提下将OA变换为与(0,1,0)y轴方向向量)平行。现在可以直接确定再次旋转所用的四元数ql,待确定的量有旋转角度θ和旋转轴u
计算旋转角度:将OA单位化,得到(13,13,13),因此

θ=arccos[(13,13,13)(0,1,0)]=arccos1354.74

旋转轴垂直于旋转前后的向量,有

u=(13,13,13)×(0,1,0)=(13,0,13)

将其单位化得(12,0,12)。如果使用轴角式,left_rotation的数据为:

snbt
left_rotation:{angle: 54.74f, axis: [-0.71f, 0.0f, 0.71f]}

计算再次旋转四元数

q=(cosθ2,uxsinθ2,uysinθ2,uzsinθ2)(0.89,0.33,0,0.33)

模型不需要进行初次旋转、缩放和平移,故qr=(1,0,0,0)s=(1,1,1)t=(0,0,0)。分解形式的transformation字段为:

snbt
transformation:{right_rotation: [0.0f, 0.0f, 0.0f, 1.0f], scale: [1.0f, 1.0f, 1.0f], left_rotation: [-0.33f, 0.0f, 0.33f, 0.89f], translation: [1.0f, 1.0f, 1.0f]}

生成这个展示实体所需的命令为:

mcfunction
summon block_display ~ ~ ~ {block_state:{Name:"minecraft:glass"},transformation:{right_rotation:[0.0f,0.0f,0.0f,1.0f],scale:[1.0f,1.0f,1.0f],left_rotation:[-0.33f,0.0f,0.33f,0.89f],translation:[1.0f,1.0f,1.0f]}}

字段left_rotation的值已定义,旋转动画由插值完成,可由right_rotation定义,待确定的量依然是旋转角度θ和旋转轴u。显然旋转轴为方块模型的体对角线,这时体对角线与y轴平行,但变换依旧基于模型的局部坐标,因此轴向量为(1,1,1),单位化为(13,13,13)。制作插值动画时,可以设定四个固定的旋转角度:090180270,使得模型依此顺序循环变换,每次插值的时长为4÷4=1=20gt。
θ=90为例,若使用轴角式,则right_rotation的数据为

snbt
right_rotation: {angle: 90, axis: [0.58f, 0.58f, 0.58f]}

转换为四元数形式q(0.71,0.41,0.41,0.41),即

snbt
right_rotation:[0.41f,0.41f,0.41f,0.71f]

同理θ=180θ=270θ=0的数据分别为

snbt
right_rotation:[0.58f,0.58f,0.58f,0.0f]}
snbt
right_rotation:[0.41f,0.41f,0.41f,-0.71f]}
snbt
right_rotation:[0.0f,0.0f,0.0f,1.0f]}

为将模型的旋转角度平滑过渡至θ=90,插值动画的命令为

mcfunction
data merge entity @n[type=block_display] {transformation:{right_rotation:[0.41f,0.41f,0.41f,0.71f]},interpolation_duration:20}

该命令执行后,应用命令方块电路或函数计划使20gt后、定义的插值动画结束时模型的旋转角度开始平滑过渡至θ=180

mcfunction
data merge entity @n[type=block_display] {transformation:{right_rotation:[0.58f,0.58f,0.58f,0.0f]},interpolation_duration:20}

20gt后开始平滑过渡至θ=270

mcfunction
data merge entity @n[type=block_display] {transformation:{right_rotation:[0.41f,0.41f,0.41f,-0.71f]},interpolation_duration:20}

20gt后开始平滑过渡至θ=0

mcfunction
data merge entity @n[type=block_display] {transformation:{right_rotation:[0.0f,0.0f,0.0f,1.0f]},interpolation_duration:20}

20gt后开始平滑过渡至θ=90,形成循环。若在命令方块电路中执行命令,则可以制造一个周期为80gt的时钟电路,每个命令方块之间需要有20gt的延迟,至少需要使用5个中继器。若在函数中执行命令,则可以在目录data\minecraft\function\animation下创建90.mcfunction180.mcfunction270.mcfunction0.mcfunction四个函数。例如,函数90.mcfunction的内容可以如下所示:

mcfunction
data merge entity @n[type=block_display] {transformation:{right_rotation:[0.41f,0.41f,0.41f,0.71f]},interpolation_duration:20}
schedule function minecraft:animation/180 20t

参考文献

[1] https://zh.minecraft.wiki/w/展示实体
[2] https://krasjet.github.io/quaternion/quaternion.pdf
[3] https://blog.csdn.net/YiYeZhiNian/article/details/106750302
[4] https://zhuanlan.zhihu.com/p/45404840
[5] https://zhuanlan.zhihu.com/p/183973440