skia库的3D坐标变换其实也是无奈之举。参照SKCamera.cpp:
首先,定义了一个虚拟相机:
void SkCamera3D::reset() { fLocation.set(0, 0, -SkIntToScalar(576)); // 8 inches backward fAxis.set(0, 0, SK_Scalar1); // forward fZenith.set(0, -SK_Scalar1, 0); // up fObserver.set(0, 0, fLocation.fZ); fNeedToUpdate = true; }
求出相机的投影矩阵。这个代码里面叫做方位(orientation)
这个方位矩阵完成了从3D空间到2D空间的投影,
这个方法研究了很久,具体分析见注释,不多讲了。
相当于OpenGL的glFrustum函数。代码如下:
void SkCamera3D::doUpdate() const { SkUnit3D axis, zenith, cross; fAxis.normalize(&axis); { SkScalar dot = SkUnit3D::Dot(*(const SkUnit3D*)(const void*)&fZenith, axis); zenith.fX = fZenith.fX - SkUnitScalarMul(dot, axis.fX); zenith.fY = fZenith.fY - SkUnitScalarMul(dot, axis.fY); zenith.fZ = fZenith.fZ - SkUnitScalarMul(dot, axis.fZ); (void)((SkPoint3D*)(void*)&zenith)->normalize(&zenith); } /* [-z, 0, x] [cross.fX, cross.fY, cross.fZ] [0, -z, y]* [zenith.fx, zenith.fY, zenith.fZ] [0, 0, 1] [axis.fx, axis.fY, axis.fZ] 下面这个矩阵式是投影矩阵: [-z, 0, x] [0, -z, y] [0, 0, 1] 投影到x,y z位置的投影矩阵,其中x, y恒为0。如果不为0,经推算, 这个矩阵是有错误的,用三角形相似,可以算出,应该是下面这个样子 [-z, 0, x-zx] [0, -z, y-zy] [0, 0, 1] */ SkUnit3D::Cross(axis, zenith, &cross); { SkMatrix* orien = &fOrientation; SkScalar x = fObserver.fX; SkScalar y = fObserver.fY; SkScalar z = fObserver.fZ; orien->set(SkMatrix::kMScaleX, SkUnitScalarMul(x, axis.fX) - SkUnitScalarMul(z, cross.fX)); orien->set(SkMatrix::kMSkewX, SkUnitScalarMul(x, axis.fY) - SkUnitScalarMul(z, cross.fY)); orien->set(SkMatrix::kMTransX, SkUnitScalarMul(x, axis.fZ) - SkUnitScalarMul(z, cross.fZ)); orien->set(SkMatrix::kMSkewY, SkUnitScalarMul(y, axis.fX) - SkUnitScalarMul(z, zenith.fX)); orien->set(SkMatrix::kMScaleY, SkUnitScalarMul(y, axis.fY) - SkUnitScalarMul(z, zenith.fY)); orien->set(SkMatrix::kMTransY, SkUnitScalarMul(y, axis.fZ) - SkUnitScalarMul(z, zenith.fZ)); orien->set(SkMatrix::kMPersp0, axis.fX); orien->set(SkMatrix::kMPersp1, axis.fY); orien->set(SkMatrix::kMPersp2, axis.fZ); } }
然后在patchToMatrix方法中,使用此投影矩阵对当前的变换UVO坐标系进行了变换,代码如下:
void SkCamera3D::patchToMatrix(const SkPatch3D& quilt, SkMatrix* matrix) const { if (fNeedToUpdate) { this->doUpdate(); fNeedToUpdate = false; } const SkScalar* mapPtr = (const SkScalar*)(const void*)&fOrientation; const SkScalar* patchPtr; SkPoint3D diff; SkScalar dot; diff.fX = quilt.fOrigin.fX - fLocation.fX; diff.fY = quilt.fOrigin.fY - fLocation.fY; diff.fZ = quilt.fOrigin.fZ - fLocation.fZ; dot = SkUnit3D::Dot(*(const SkUnit3D*)(const void*)&diff, *(const SkUnit3D*)(((const SkScalar*)(const void*)&fOrientation) + 6)); // patchPtr的结构为{U,V,ORIGIN}其中U,V 代表列向量 // ORIGIN 是坐标原点 patchPtr = (const SkScalar*)&quilt; /* 其中matrix表示一个3x3的矩阵 第一行代表U的系数,第二行是V的系数,第三行是diff 的系数 matrix 的每一列代表的是一个坐标轴。 */ //第一列 matrix->set(SkMatrix::kMScaleX, SkScalarDotDiv(3, patchPtr, 1, mapPtr, 1, dot)); matrix->set(SkMatrix::kMSkewY, SkScalarDotDiv(3, patchPtr, 1, mapPtr+3, 1, dot)); matrix->set(SkMatrix::kMPersp0, SkScalarDotDiv(3, patchPtr, 1, mapPtr+6, 1, dot)); //第二列 patchPtr += 3; matrix->set(SkMatrix::kMSkewX, SkScalarDotDiv(3, patchPtr, 1, mapPtr, 1, dot)); matrix->set(SkMatrix::kMScaleY, SkScalarDotDiv(3, patchPtr, 1, mapPtr+3, 1, dot)); matrix->set(SkMatrix::kMPersp1, SkScalarDotDiv(3, patchPtr, 1, mapPtr+6, 1, dot)); //第三列 patchPtr = (const SkScalar*)(const void*)&diff; matrix->set(SkMatrix::kMTransX, SkScalarDotDiv(3, patchPtr, 1, mapPtr, 1, dot)); matrix->set(SkMatrix::kMTransY, SkScalarDotDiv(3, patchPtr, 1, mapPtr+3, 1, dot)); matrix->set(SkMatrix::kMPersp2, SK_UnitScalar1); }
到此为止,也就把这个变换的思路给理解清楚了。跟OpenGL的思路类似。
第一。进行模型视图变换,通过一个patch的东西,UVO坐标系。
第二。进行投影变换。定位相机,具体参照如下函数:
/* 由于x,y,z 与cross的点积,就是在cross方向上的投影。因此,对于x,y,z向量来说。 从世界坐标转换到相机坐标系的矩阵就是 [cross.fX, cross.fY, cross.fZ] [zenith.fx, zenith.fY, zenith.fZ] [axis.fx, axis.fY, axis.fZ] 由于是标准正交基, 他的转置或者说,逆矩阵,就是相机矩阵 [cross.fX, zenith.fx, axis.fx] [cross.fY, zenith.fY, axis.fY] [ cross.fZ ,zenith.fZ, axis.fZ] [-z, 0, x] [cross.fX, cross.fY, cross.fZ] [0, -z, y]* [zenith.fx, zenith.fY, zenith.fZ] [0, 0, 1] [axis.fx, axis.fY, axis.fZ] 下面这个矩阵式是投影矩阵: [-z, 0, x] [0, -z, y] [0, 0, 1] */
增加一些注释
下面增加一些系统介绍
SKCamera3D中关于模拟3D的算法研究
SkCamera是一个可以支持3D变换的组件,通过一些矩阵变换,最终获得了3D的效果。与OpenGL的思路基本相同,分为两个矩阵,一个是投影矩阵,一个是模型视图矩阵。
下面就这两个矩阵,并结合SkCamera的代码进行讲解。
1. 投影矩阵。
投影矩阵是通过SkCamera3D这个类进行生成和管理。SkCamera3D这个的作用还在于将一个SkPatch3D的一个坐标系转换为投影后的坐标系,通过patchToMatrix完成。
总而言之,这个类负责投影的相关工作。
class SkCamera3D {
public:
SkCamera3D();
void reset();
void update();
void patchToMatrix(const SkPatch3D&, SkMatrix* matrix) const;
SkPoint3D fLocation;
SkPoint3D fAxis;
SkPoint3D fZenith;
SkPoint3D fObserver;
private:
mutable SkMatrix fOrientation;
mutable bool fNeedToUpdate;
void doUpdate() const;
};
2. 基本投影算法
世界坐标系如图所示
X |
Z |
Y |
O |
相机方向
相机顶的方向
然后在这个世界坐标系中,根据想要的投影方式,初始化了一个相机(类似于OpenGL中的glFrustum函数)。相机顶朝下,相机的摄像头朝着屏幕里面。
对应SkCamera.cpp的代码如下:
void SkCamera3D::reset() {
fLocation.set(0, 0, -SkIntToScalar(576)); // 8 inches backward
fAxis.set(0, 0, SK_Scalar1); // forward
fZenith.set(0, -SK_Scalar1, 0); // up
fObserver.set(0, 0, fLocation.fZ);
fNeedToUpdate = true;
}
然后根据相机的位置获得一个投影矩阵。
公式如下:
【相机投影矩阵】*【相机世界矩阵】
根据2维齐次坐标的几何意义,也就是在Z=1平面上的投影。现在变为在Z=-z平面上的投影。具体参照如下示意图:这个图也展示了二维齐次坐标系的几何意义(投影+映射):
Z=0
P |
Z=1 |
Z=-z |
-z, 0, x
0, -z, y
0, 0, 1
具体可以验证这个,假设x’,y’,z’
那么,可得:
X’’ = -x’z + xz’
Y’’ = -y’z + yz’
Z’’ = z’
进而得到了相机的坐标
X’’ = -x’z/z’ + x
Y’’ = -y’z/z’ + y
Z’’ = 1
另外,从三角形相似原理可以知道:
X’’ = -x’z/z’ 和Y’’ = -y’z/z’刚好把坐标刚好映射到了z平面上。然后进行平移,再加上相机的位置x,y 从而得到如下公式:
X’’ = -x’z/z’ + x
Y’’ = -y’z/z’ + y
这两个公式是相同的,从而验证了映射的正确性。
下面就是投影矩阵的具体实现方法:
void SkCamera3D::doUpdate() const {
SkUnit3D axis, zenith, cross;
fAxis.normalize(&axis);
{
SkScalar dot = SkUnit3D::Dot(*(const SkUnit3D*)(const void*)&fZenith, axis);
zenith.fX = fZenith.fX - SkUnitScalarMul(dot, axis.fX);
zenith.fY = fZenith.fY - SkUnitScalarMul(dot, axis.fY);
zenith.fZ = fZenith.fZ - SkUnitScalarMul(dot, axis.fZ);
(void)((SkPoint3D*)(void*)&zenith)->normalize(&zenith);
}
SkUnit3D::Cross(axis, zenith, &cross);
{
SkMatrix* orien = &fOrientation;
SkScalar x = fObserver.fX;
SkScalar y = fObserver.fY;
SkScalar z = fObserver.fZ;
orien->set(SkMatrix::kMScaleX, SkUnitScalarMul(x, axis.fX) - SkUnitScalarMul(z, cross.fX));
orien->set(SkMatrix::kMSkewX, SkUnitScalarMul(x, axis.fY) - SkUnitScalarMul(z, cross.fY));
orien->set(SkMatrix::kMTransX, SkUnitScalarMul(x, axis.fZ) - SkUnitScalarMul(z, cross.fZ));
orien->set(SkMatrix::kMSkewY, SkUnitScalarMul(y, axis.fX) - SkUnitScalarMul(z, zenith.fX));
orien->set(SkMatrix::kMScaleY, SkUnitScalarMul(y, axis.fY) - SkUnitScalarMul(z, zenith.fY));
orien->set(SkMatrix::kMTransY, SkUnitScalarMul(y, axis.fZ) - SkUnitScalarMul(z, zenith.fZ));
orien->set(SkMatrix::kMPersp0, axis.fX);
orien->set(SkMatrix::kMPersp1, axis.fY);
orien->set(SkMatrix::kMPersp2, axis.fZ);
}
}
3. 模型变换矩阵的生成。
模型变换是通过如下结构体完成的。
struct SkMatrix3D {
SkScalar fMat[3][4];
void reset();
void setRow(int row, SkScalar a, SkScalar b, SkScalar c, SkScalar d = 0)
{
SkASSERT((unsigned)row < 3);
fMat[row][0] = a;
fMat[row][1] = b;
fMat[row][2] = c;
fMat[row][3] = d;
}
void setRotateX(SkScalar deg);
void setRotateY(SkScalar deg);
void setRotateZ(SkScalar deg);
void setTranslate(SkScalar x, SkScalar y, SkScalar z);
void preRotateX(SkScalar deg);
void preRotateY(SkScalar deg);
void preRotateZ(SkScalar deg);
void preTranslate(SkScalar x, SkScalar y, SkScalar z);
void setConcat(const SkMatrix3D& a, const SkMatrix3D& b);
void mapPoint(const SkPoint3D& src, SkPoint3D* dst) const;
void mapVector(const SkVector3D& src, SkVector3D* dst) const;
void mapPoint(SkPoint3D* v) const
{
this->mapPoint(*v, v);
}
void mapVector(SkVector3D* v) const
{
this->mapVector(*v, v);
}
};
这样得到的矩阵式3D世界的3*4的矩阵。而投影矩阵只能对3*3的矩阵进行变换。而patch构成的坐标系却是3*3的。无法相乘。那么程序如何实现patch从3*4 到 3*3的一个转换呢。
下面是推导过程:
Ux Vx Nx Dx
Uy Vy Ny Dy
Uz Vz Nz Dz
0 0 0 1
这个就是patch坐标系对应的3D世界的变换矩阵。其中U,V,N为基向量。D为坐标原点。
我们称之为D-UVN坐标系。
最重要的信息是,在D-UVN坐标系中,N轴上的值横为0.因为一开始,一个坐标x,y一定是平面的。因此,只需要求出对【x,y, 0, 1】的一个变换就行了。
可以得出:
x‘= Ux*x + Vx*y + Dx
y’ =Uy*x + Vy*y + Dy
z’=Uz*x + Vz*y + Dz
也就是如下矩阵:
Ux Vx Dx
Uy Vy Dy
Uz Vz Dz
代码如下:
void SkCamera3D::patchToMatrix(const SkPatch3D& quilt, SkMatrix* matrix) const {
if (fNeedToUpdate) {
this->doUpdate();
fNeedToUpdate = false;
}
const SkScalar* mapPtr = (const SkScalar*)(const void*)&fOrientation;
const SkScalar* patchPtr;
SkPoint3D diff;
SkScalar dot;
diff.fX = quilt.fOrigin.fX - fLocation.fX;
diff.fY = quilt.fOrigin.fY - fLocation.fY;
diff.fZ = quilt.fOrigin.fZ - fLocation.fZ;
dot = SkUnit3D::Dot(*(const SkUnit3D*)(const void*)&diff,
*(const SkUnit3D*)(((const SkScalar*)(const void*)&fOrientation) + 6));
patchPtr = (const SkScalar*)&quilt;
matrix->set(SkMatrix::kMScaleX, SkScalarDotDiv(3, patchPtr, 1, mapPtr, 1, dot));
matrix->set(SkMatrix::kMSkewY, SkScalarDotDiv(3, patchPtr, 1, mapPtr+3, 1, dot));
matrix->set(SkMatrix::kMPersp0, SkScalarDotDiv(3, patchPtr, 1, mapPtr+6, 1, dot));
patchPtr += 3;
matrix->set(SkMatrix::kMSkewX, SkScalarDotDiv(3, patchPtr, 1, mapPtr, 1, dot));
matrix->set(SkMatrix::kMScaleY, SkScalarDotDiv(3, patchPtr, 1, mapPtr+3, 1, dot));
matrix->set(SkMatrix::kMPersp1, SkScalarDotDiv(3, patchPtr, 1, mapPtr+6, 1, dot));
patchPtr = (const SkScalar*)(const void*)&diff;
matrix->set(SkMatrix::kMTransX, SkScalarDotDiv(3, patchPtr, 1, mapPtr, 1, dot));
matrix->set(SkMatrix::kMTransY, SkScalarDotDiv(3, patchPtr, 1, mapPtr+3, 1, dot));
matrix->set(SkMatrix::kMPersp2, SK_UnitScalar1);
}
总结:
Sk3DCamera提供了一种方法,可以对图形进行仿3D变换,这对于想在2D空间进行3D处理的程序来说,是一个不错的选择。