Cocos2d摄像机详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wlk1229/article/details/82750104

Cocos2d摄像机详解

摄像机的作用

在3D系统中都会有一个摄像机的概念,物体在显示之前需要先将物体的坐标转换到视角坐标,也就是摄像机坐标,然后再投影,最后还需对投影画面进行缩放到视口显示的大小。Coscos2d-x 3.x支持3D的,所以其肯定会有摄像机。在Cocos中摄像机由类Camera实现,主要作用有3个:1、设置视口,2、将全局坐标转换到摄像机坐标中,3投影。

 

设置视口

设置视口作用是设置渲染结果最终的显示方式、显示位置以及显示大小。渲染结果不光可以显示在屏幕的窗口上,也可以写入帧缓存中,Camera类支持这两种方式。摄像机默认情况显示到窗口,可以通过函数Camera::setFrameBufferObject设置渲染到帧缓存。视口位置和大小决定可最后投影生成的图像显示的位置和大小,所有显示到窗口的摄像机都使用同一个视口static Viewport Camera::_defaultViewport,可以通过函数static Camera::setDefaultViewport设置,帧缓存的视口可以通过函数Camera::setViewport设置。

Cocos也支持场景多视口显示,可以显示一个物体的多个视角,例如同时显示物体的正面视图和俯视图。

 

摄像机坐标

物体投影显示之前需要转换到摄像机坐标,这个功能在Cocos中实现还是比较简单的。在Cocos中每个继承至Node的UI节点都有局部坐标系,摄像机坐标系本身也只是一个局部坐标系,所以Camera只需要继承Node节点就可以实现该功能。具体坐标转换解释和原理请参考:Cocos2d-x 坐标系统详解

为了更直观的设置摄像机,可以函数Camera::lookAt设置摄像机的方向。

 

投影

投影的作用是将3D物体,转换成2D坐标,转换后物体x,y坐标值都会压缩在[-1.0, 1.0]。显示的时候会将坐标拉伸到视口大小显示。

投影的方式有两种,一种是物体离摄像机越远越小,这种为透视投影,另一中是,远近物体都一样大的正交投影,默认情况下摄像机使用的是透视投影。

透视投影可以使用函数Camera::initPerspective设置。

正交投影可以使用函数Camera::initDefault设置。

 

2D显示

Cocos中使用Z=0平面显示2D元素,2D精灵、菜单、Label等z轴的值都为0。为了防止2D元素投影后图像会被拉伸或压缩,当视口宽高为w,h时,会将z=0平面的物体位置在(-w/2, -h/2)投影为(-1, -1),(w/2, h/2)投影为(1, 1) ,摄像机的位置为(w/2, h/2)。一下是默认摄像机初始化代码:

bool Camera::initDefault()

{

    auto size = Director::getInstance()->getWinSize();

    //create default camera

    auto projection = Director::getInstance()->getProjection();

    switch (projection)

    {

        case Director::Projection::_2D:

        {

            initOrthographic(size.width, size.height, -1024, 1024);

            setPosition3D(Vec3(0.0f, 0.0f, 0.0f));

            setRotation3D(Vec3(0.f, 0.f, 0.f));

            break;

        }

        case Director::Projection::_3D:

        {

            float zeye = Director::getInstance()->getZEye();

            initPerspective(60, (GLfloat)size.width / size.height, 10, zeye + size.height / 2.0f);

            Vec3 eye(size.width/2, size.height/2.0f, zeye), center(size.width/2, size.height/2, 0.0f), up(0.0f, 1.0f, 0.0f);

            setPosition3D(eye);

            lookAt(center, up);

            break;

        }

        default:

            CCLOG("unrecognized projection");

            break;

    }

    return true;

}

 

注意代码initPerspective的参数

float zeye = Director::getInstance()->getZEye();

initPerspective(60, (GLfloat)size.width / size.height, 10, zeye + size.height / 2.0f);

Vec3 eye(size.width/2, size.height/2.0f, zeye),

setPosition3D(eye);

Zeye的值如下

float Director::getZEye(void) const

{

    return (_winSizeInPoints.height / 1.154700538379252f);//(2 * tanf(M_PI/6))

}

 

从代码中也可以看到近裁平面和远裁平面分别为10和zeye + size.height / 2.0f。这里可以看出默认情况下Cocos的3D显示超过2D平面后面size.height / 2.0f位置的内容将被裁剪。

 

使用摄像机

渲染与摄像机

Camera类本身是一个Node节点,可以通过addChild加入到场景树中任何节点上。每个摄像机都属于一个Scene,存放在变量 Scene* Camera::_scene中,在Scene类中存放着一个数组std::vector<Camera*> Scene::_cameras,存放了Scene下所有的摄像机。

当场景第一次渲染时会调用所有节点的Node::onEnter函数,Camera类的OnEnter函数中会寻摄像机所属的场景,让还将摄像机加入到场景摄像机数组中。代码如下:

void Camera::onEnter()

{

    if (_scene == nullptr)

    {

        auto scene = getScene();

        if (scene)

        {

            setScene(scene);

        }

    }

    Node::onEnter();

}

 

Cocos的渲染都是在函数Scene::render中,在Scene::render中会为当前场景中的每个摄像机执行一次渲染。

void Scene::render(Renderer* renderer, const Mat4* eyeTransforms, const Mat4* eyeProjections, unsigned int multiViewCount)
{
    auto director = Director::getInstance();
    Camera* defaultCamera = nullptr;

    for (const auto& camera : getCameras())
    {
        if (!camera->isVisible())
            continue;

        Camera::_visitingCamera = camera;
        if (Camera::_visitingCamera->getCameraFlag() == CameraFlag::DEFAULT)
        {
            defaultCamera = Camera::_visitingCamera;
        }
        
        for (unsigned int i = 0; i < multiViewCount; ++i) {
            if (eyeProjections)
                camera->setAdditionalProjection(eyeProjections[i] * camera->getProjectionMatrix().getInversed());
            if (eyeTransforms)
                camera->setAdditionalTransform(eyeTransforms[i].getInversed());
            director->pushProjectionMatrix(i);
            director->loadProjectionMatrix(Camera::_visitingCamera->getViewProjectionMatrix(), i);
        }

        camera->apply();
        //clear background with max depth
        camera->clearBackground();
        //visit the scene
        visit(renderer, transform, 0);

        renderer->render();
        camera->restore();

        for (unsigned int i = 0; i < multiViewCount; ++i)
            director->popProjectionMatrix(i);
    }
    ………
    Camera::_visitingCamera = nullptr;
}

每个摄像机渲染时还有有一个multiViewCount循环,这里是设置视口矩阵,多次是因为可以设置多个视口,默认情况下这个循环只执行一次。循环中最后两行代码:

director->pushProjectionMatrix(i);

director->loadProjectionMatrix(Camera::_visitingCamera->getViewProjectionMatrix(), i);

这两行代码时将摄像机的投影视图矩阵(投影矩阵和视图矩阵相乘的结果)压入到导演类的中栈中。

在执行渲染命令时,会从栈中取出该值,设置Uniform值,具体函数为GLProgram::setUniformsForBuiltins,代码如下:

void GLProgram::setUniformsForBuiltins(const Mat4 &matrixMV)

{

    const auto& matrixP = _director->getMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);

 

    if (_flags.usesP)

        setUniformLocationWithMatrix4fv(_builtInUniforms[UNIFORM_P_MATRIX], matrixP.m, 1);

 

……

 

    if (_flags.usesMVP)

    {

        Mat4 matrixMVP = matrixP * matrixMV;

        setUniformLocationWithMatrix4fv(_builtInUniforms[UNIFORM_MVP_MATRIX], matrixMVP.m, 1);

    }

……

}

 

 

摄像机掩码

每个摄像机都可以设置一个CameraFlag,这是一个位枚举,

enum class CameraFlag

{

    DEFAULT = 1,

    USER1 = 1 << 1,

    USER2 = 1 << 2,

    USER3 = 1 << 3,

    USER4 = 1 << 4,

    USER5 = 1 << 5,

    USER6 = 1 << 6,

    USER7 = 1 << 7,

    USER8 = 1 << 8,

};

UI节点可以通过函数Node::setCameraMask设置摄像机掩码,可以使用或运算(“|”)同时设置多个掩码,这样可以同时在多个摄像机下可以,从而绘制多次。

 

绘制时可以将3D物体全部使用一个掩码,例如CameraFlag::USER1,然后单独设置一个摄像机并设置标识为CameraFlag::USER1,这样3D显示和2D显示可以分别使用两个摄像机控制,这样可以更容易的控制3D和2D的绘制。

 

摄像机使用步骤

1、创建摄像机,通过函数Camera::create()创建

2、设置摄像机位置,方向,视角(投影),分别通过函数Node::setPositionCamera::lookAtCamera::initPerspective设置。

3、设置摄像机表示,使用函数Camera::setCameraFlag

4、将摄像机添加到场景中。

 

3D透明

3D物体在绘制的时候使用深度缓存进行遮挡测试,无需关系先后绘制,所以不需要使用localZOrder_globalOrder排序。但是3D物体如果有透明,只有按照物体在摄像机坐标系下Z轴由小到大绘制才可以正确显示透明。Cocos在绘制透明3D物体时,会生成一个_depth值,这个值代表了物体摄像机坐标系下z轴的值,绘制前,会使用该值对命令进行排序。

 

生成这个_depth值会使用到函数Camera::getDepthInView,代码如下:

float Camera::getDepthInView(const Mat4& transform) const
{
    Mat4 camWorldMat = getNodeToWorldTransform();
    const Mat4 &viewMat = camWorldMat.getInversed();
    float depth = -(viewMat.m[2] * transform.m[12] + viewMat.m[6] * transform.m[13] + viewMat.m[10] * transform.m[14] + viewMat.m[14]);
    return depth;
}

函数返回值depth是transform最后一列作为一个点,只进行位移转换到摄像机坐标系下,z轴的值。

绘制命令排序有如下代码:

std::stable_sort(std::begin(_commands[QUEUE_GROUP::TRANSPARENT_3D]), 
                 std::end(_commands[QUEUE_GROUP::TRANSPARENT_3D]), compare3DCommand);
static bool compare3DCommand(RenderCommand* a, RenderCommand* b)
{
    return  a->getDepth() > b->getDepth();
}

猜你喜欢

转载自blog.csdn.net/wlk1229/article/details/82750104