如何从投影和模型视图矩阵中获取CATransform3D?

6
大家好,

我有一个使用OpenGL-ES绘制3D模型的iPhone项目,需要根据给定的模型视图矩阵和投影矩阵。现在我需要用CALayer替换3D模型,因此将模型视图矩阵的值放入CATransform3D结构中,并将其分配给layer.transform。它能够正常工作,图层被显示在屏幕上并按预期移动,但是过了一段时间,我意识到我的图层行为不够精确,应该考虑投影矩阵。然而问题来了:当我简单地连结两个矩阵时,我的图层看起来很奇怪(非常小,约为2像素,而实际上应该约为300,因为它非常远),或者根本看不见。我该如何解决它呢?

以下是代码片段:

- (void)adjustImageObjectWithUserInfo:(NSDictionary *)userInfo
{
    NSNumber *objectID = [userInfo objectForKey:kObjectIDKey];
    CALayer *layer = [self.imageLayers objectForKey:objectID];
    if (!layer) { return; }

    CATransform3D transform = CATransform3DIdentity;
    NSArray *modelViewMatrix = [userInfo objectForKey:kModelViewMatrixKey];

      // Get raw model view matrix;
    CGFloat *p = (CGFloat *)&transform;
    for (int i = 0; i < 16; ++i)
    {
        *p = [[modelViewMatrix objectAtIndex:i] floatValue];
        ++p;
    }

      // Rotate around +z for Pi/2 
    transform = CATransform3DConcat(transform, CATransform3DMakeRotation(M_PI_2, 0, 0, 1));

      // Project with projection matrix
    transform = CATransform3DConcat(transform, _projectionMatrix);

    layer.transform = transform; 
}

任何帮助都将不胜感激。


你最终成功了吗?我也在尝试做同样的事情——将OpenGL投影和modelView转换为CATransform3D,并附加到CATransformLayer上。 - Tony Arnold
5个回答

10

我最近也遇到了这个问题(我也在使用ARToolKit),看到你没有找到答案我感到非常失望。虽然我想你现在可能已经解决了这个问题,但我已经弄清楚了并将其发布给任何其他可能遇到此问题的人。

对我来说最困惑的是,每个人都谈论通过将m34变量设置为小的负数来创建CALayer透视转换。虽然这确实可以工作,但并不十分有信息量。我最终意识到,该转换与其他所有转换完全相同,它是用于齐次坐标的列主要变换矩阵。唯一的特殊情况是它必须组合模型视图和投影矩阵,然后在一个矩阵中缩放到openGL视口的大小。我开始尝试使用类似于m34是小的负数这样的矩阵风格,详细说明可在这里找到,但最终切换到使用OpenGL风格的透视变换,如这里所述。它们事实上等效,只是表示变换的不同方式。

在我们的情况下,我们试图使CALayer变换完全复制openGL变换。这仅需要将模型视图、投影和缩放矩阵相乘,并翻转y轴以解决设备屏幕原点位于左上角而openGL原点位于左下角的问题。只要图层锚点在(.5,.5)处,其位置恰好位于屏幕中心,结果就与openGL的变换相同。

void attach_CALayer_to_marker(CATransform3D* transform, Matrix4 modelView, Matrix4 openGL_projection, Vector2 GLViewportSize)
{
//Important: This function assumes that the CA layer has its origin in the 
//exact center of the screen.

Matrix4 flipY = {   1, 0,0,0,
                    0,-1,0,0,
                    0, 0,1,0,
                    0, 0,0,1}; 

//instead of -1 to 1 we want our result to go from -width/2 to width/2, same 
//for height
CGFloat ScreenScale = [[UIScreen mainScreen] scale];
float xscl = GLViewportSize.x/ScreenScale/2;
float yscl = GLViewportSize.y/ScreenScale/2;

//The open GL perspective matrix projects onto a 2x2x2 cube.  To get it onto the
    //device screen it needs to be scaled to the correct size but
//maintaining the aspect ratio specified by the open GL window.
    Matrix4 scalingMatrix = {xscl,0   ,0,0,
                               0,   yscl,0,0,
                               0,   0   ,1,0,
                               0,   0   ,0,1};

//Open GL measures y from the bottom and CALayers measure from the top so at the
//end the entire projection must be flipped over the xz plane.
//When that happens the contents of the CALayer will get flipped upside down.
//To correct for that they are flipped updside down at the very beginning,
//they will then be flipped right side up at the end.

Matrix flipped = MatrixMakeFromProduct(modelView, flipY);
Matrix unscaled = MatrixMakeFromProduct(openGL_projection, flipped);
Matrix scaled = MatrixMakeFromProduct(scalingMatrix, unscaled);

//flip over xz plane to move origin to bottom instead of top
Matrix Final = SiMatrix4MakeFromProduct(flipY, scaled);
*transform = convert_your_matrix_object_to_CATransform3D(Final);
}
该函数使用OpenGL投影和视图大小来生成正确的变换CALayer。 CALayer的大小应以OpenGL场景的单位指定。 OpenGL视口实际上包含4个变量[xoffset,yoffset,x,y],但前两个不相关,因为CALayer的原点放在屏幕中心以对应于OpenGL 3D原点。
只需将矩阵替换为您可以访问的任何通用4x4列主要矩阵类。任何东西都可以正常工作,只需确保以正确的顺序乘以矩阵。所有这些本质上都是复制OpenGL管道(减去剪辑)。

感谢您的回答,这个问题困扰了我很久。我会尝试您的方法并做出回应。 - Zapko
我也在使用ARToolkit,但是我无法弄清楚你是如何设置Matrix4和Vector2的。这是我需要的结构定义吗?此外,这应该放在ARView中还是可以留在ARViewController中?我一直在努力让它工作:)看起来你已经做到了,但是我没有更多的信息就无法复制。 - Jim True
@JimTrue 这些只是我项目使用的自定义矩阵库中的通用矩阵结构。如果你的项目还没有使用矩阵库,我建议你找到一个现有的库,而不是自己编写一个。我相信有很多选择,矩阵乘法是一个非常常见的用例。 - Hammer

2
我刚刚摆脱了投影矩阵,这是我得到的最好的结果:
- (void)adjustTransformationOfLayerWithMarkerId:(NSNumber *)markerId forModelViewMatrix:(NSArray *)modelViewMatrix
{   
    CALayer *layer = [self.imageLayers objectForKey:markerId];

            ...

    CATransform3D transform = CATransform3DIdentity;        
    CGFloat *p = (CGFloat *)&transform;
    for (int i = 0; i < 16; ++i) {
        *p = [[modelViewMatrix objectAtIndex:i] floatValue];
        ++p;
    }

    transform.m44 = (transform.m43 > 0) ? transform.m43/kZDistanceWithoutDistortion : 1;

    CGFloat angle = -M_PI_2;
    if (self.delegate.interfaceOrientation == UIInterfaceOrientationLandscapeLeft)      { angle = M_PI; }
    if (self.delegate.interfaceOrientation == UIInterfaceOrientationLandscapeRight)     { angle = 0; }
    if (self.delegate.interfaceOrientation == UIInterfaceOrientationPortraitUpsideDown) { angle = M_PI_2; }
    transform = CATransform3DConcat(transform, CATransform3DMakeRotation(angle, 0, 0, -1));

    transform = CATransform3DConcat(CATransform3DMakeScale(-1, 1, 1), transform);

        // Normalize transformation
    CGFloat scaleFactor = 1.0f / transform.m44;
    transform.m41 = transform.m41 * scaleFactor;
    transform.m42 = transform.m42 * scaleFactor;
    transform.m43 = transform.m43 * scaleFactor;
    transform = CATransform3DScale(transform, scaleFactor, scaleFactor, scaleFactor);
    transform.m44 = 1;

    BOOL disableAction = YES;

            ...

    [CATransaction begin];
    [CATransaction setDisableActions:disableAction]; // Disable animation for layer to move faster
    layer.transform = transform;                
    [CATransaction commit];
}

虽然不是完全精确,但对于我的目的来说已经足够准确了。当x或y位移大约达到屏幕大小时,偏转变得明显。


谢谢@Zapko。我尝试了你的代码,但是对于我的标记转换(你使用String SDK吗?),我没有得到任何有用的结果。我想我需要坐下来手动分析一下我所接收到的转换实际上在做什么。 - Tony Arnold
嗨,托尼!不,我正在使用ARToolKit +。我现在没有链接,但稍后我会在这里写下关于我作为基础的项目。如果您能让我知道您将获得的结果,那就太好了。 - Zapko
这是一个链接,指向那个增强现实项目: http://www.benjaminloulier.com/articles/virtual-reality-on-iphone-code-inside - Zapko
据我所知,CATransform3D矩阵与OpenGL变换矩阵相比实际上是转置的。 - Mihai Timar
非常感谢,它帮了我很大的忙 :-) 尽管我没有理解这个概念,但它确实帮助了我很多。请问您能否解释一下那四个畸变值以及 fx、fy、cx 和 cy 是什么意思? - pradeepa

1

可能存在两个问题:

1)拼接顺序。经典的矩阵数学是从右到左的。因此,请尝试

CATransform3DConcat(_projectionMatrix, transform)

2) 投影系数的值是错误的。您使用的是什么值?

希望这可以帮助到您。


嗨,Eric!是的,我尝试了不同的连接顺序,很多次。你说的投影系数是什么意思?我觉得我可能漏掉了什么? - Zapko
然而,文档中正确的顺序是从左到右为函数。例如,CATransform3DConcat(CATransform3DConcat(model, view), projection) 将按预期工作。逆向工程。 - dev_null

0
作为对问题的直接回答:投影矩阵旨在输出-1 ... 1范围内的内容,而CoreAnimation通常使用像素。由此产生了问题。因此,在将模型视图乘以投影矩阵之前,您应该将模型缩小到适合-1 ... 1范围内(根据您的世界是什么,您可以将其分成bounds.size),并在投影矩阵乘法之后,您可以返回到像素。

因此,以下是Swift代码片段(我个人讨厌Swift,但我的客户喜欢它),我相信它可以被理解。它是为CoreAnimation和ARKit编写的,并经过测试可以正常工作。我希望ARKit矩阵可以被认为是OpenGL矩阵。

func initCATransform3D(_ t_ : float4x4) -> CATransform3D
{
    let t = t_.transpose //surprise m43 means 4th column 3rd row, thanks to Apple for amazing documenation for CATransform3D and total disregarding standard math with Mij notation
    //surprise: at Apple didn't care about CA & ARKit compatibility so no any easier way to do this
    return CATransform3D(
        m11: CGFloat(t[0][0]), m12: CGFloat(t[1][0]), m13: CGFloat(t[2][0]), m14: CGFloat(t[3][0]),
        m21: CGFloat(t[0][1]), m22: CGFloat(t[1][1]), m23: CGFloat(t[2][1]), m24: CGFloat(t[3][1]),
        m31: CGFloat(t[0][2]), m32: CGFloat(t[1][2]), m33: CGFloat(t[2][2]), m34: CGFloat(t[3][2]),
        m41: CGFloat(t[0][3]), m42: CGFloat(t[1][3]), m43: CGFloat(t[2][3]), m44: CGFloat(t[3][3])
    )
}

override func updateAnchors(frame: ARFrame) {
    for animView in stickerViews {
        guard let anchor = animView.anchor else { continue }

        //100 here to make object smaller... on input it's in pixels, but we want it to be more real.
        //we work in meters at this place
        //surprise: nevertheless the matrices are column-major they are inited in "transposed" form because arrays/columns are written in horizontal direction
        let mx = float4x4(
            [1/Float(100), 0, 0, 0],
            [0, -1/Float(100), 0, 0],    //flip Y axis; it's directed up in 3d world while for CA on iOS it's directed down
            [0, 0, 1, 0],
            [0, 0, 0, 1]
        )

        let simd_atr = anchor.transform * mx //* matrix_scale(1/Float(bounds.size.height), matrix_identity_float4x4)
        var atr = initCATransform3D(simd_atr)   //atr = anchor transform

        let camera = frame.camera
        let view = initCATransform3D(camera.viewMatrix(for: .landscapeRight))
        let proj = initCATransform3D(camera.projectionMatrix(for: .landscapeRight, viewportSize: camera.imageResolution, zNear: 0.01, zFar: 1000))

        //surprise: CATransform3DConcat(a,b) is equal to mathematical b*a, documentation in apple is wrong
        //for fun it's distinct from glsl or opengl multiplication order
        atr = CATransform3DConcat(CATransform3DConcat(atr, view), proj)
        let norm = CATransform3DMakeScale(0.5, -0.5, 1)     //on iOS Y axis is directed down, but we flipped it formerly, so return back!
        let shift = CATransform3DMakeTranslation(1, -1, 0)  //we should shift it to another end of projection matrix output before flipping
        let screen_scale = CATransform3DMakeScale(bounds.size.width, bounds.size.height, 1)
        atr = CATransform3DConcat(CATransform3DConcat(atr, shift), norm)
        atr = CATransform3DConcat(atr, CATransform3DMakeAffineTransform(frame.displayTransform(for: self.orientation(), viewportSize: bounds.size)));
        atr = CATransform3DConcat(atr, screen_scale)    //scale back to pixels

        //printCATransform(atr)
        //we assume center is in 0,0 i.e. formerly there was animView.layer.center = CGPoint(x:0, y:0)
        animView.layer.transform = atr
    }
}

顺便说一句,我认为对于那些创建了所有可能的左右手坐标系混合、Y和Z轴方向、列主矩阵、float和CGFloat不兼容以及缺乏CA和ARKit集成的开发人员来说,地狱的位置将会特别炽热...


0
你是否考虑到层会渲染到已应用正交投影矩阵的GL上下文中?
请查看在Mac上关于此类的介绍性注释;虽然在iPhone上这个类是私有的,但原理是相同的。
另外,请注意OpenGL矩阵在内存中与CATransform3D矩阵的转置方式不同。也要考虑这一点;虽然大部分结果看起来都相同,但有些可能并非如此。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接