找回密码
 立即注册
查看: 242|回复: 2

Unity 渲染学习:单相机分屏(Split Screen)

[复制链接]
发表于 2023-2-28 10:36 | 显示全部楼层 |阅读模式
分屏(Split Screen)是指在一帧中绘制不同视角下的画面,呈现在屏幕的特定位置上。
根据不同的业务需求,屏幕可以划分成不同的形状,绘制的物体可能在不同的场景中。本次尝试中,物体在同一场景内,屏幕按左右均分。
多相机分屏

最简单的方式是使用多个相机分别绘制,以下是来自ChatGPT的答案:
在Unity中实现左右分屏可以通过以下步骤和代码:

创建两个相机,将每个相机的Viewport Rect设置为不同的位置,例如第一个相机:(0, 0, 0.5, 1) ,第二个相机:(0.5, 0, 0.5, 1)。
添加脚本,分别将这两个相机绑定到不同的游戏物体上。
具体代码如下:
public Camera[] cameras;  // Store references to the two cameras

void Start () {
    cameras = GetComponents<Camera>();  
    cameras[0].rect = new Rect(0.0f, 0.0f, 0.5f, 1.0f);
    cameras[1].rect = new Rect(0.5f, 0.0f, 0.5f, 1.0f);
}
大概是这么回事。
单光源下的效果如下,因黄色方块在左边相机的远裁剪面以外,所以看不见。



单光源下,两相机分屏

再问ChatGPT:“只用一个相机如何实现左右分屏?”,没有得到有价值的答案。既然人工智能没答案,那就人工搞定吧。
单相机分屏

思路

在同一场景中,之所以同一物体(世界坐标没有变化)呈现在左边和右边屏幕的效果不同,是因为左右屏幕的观察者(相机参数)不同导致的,包括:

  • 物体与相机的相对关系改变,体现在ViewMatrix;
  • 相机四棱台参数改变,体现在ProjectionMatrix;
  • 视口参数改变,体现在ViewportMatrix
那么,先按照左边屏幕需要的相机参数设置变换矩阵,画一遍物体,然后按照右边屏幕需要的相机参数设置变换矩阵,再画一遍,就实现了左右分屏。
同时,因产生阴影的光源是同一个,那么ShadowMap只需要生成一次。
在URP管线中先实现单光源的效果,Culling和多光源的处理和存在的问题,放在后面展开。
实现

场景中保留唯一主相机,主要是完成Culling和ShadowMap,所以四棱台的参数应该合适,覆盖所有想要的物体。
创建一个ScriptableRendererFeature。
定义CameraSettings类,用于存储左右相机的不同参数。
[System.Serializable]
public class CameraSettings
{
    public Vector3 position;
    public Vector3 rotation;
    public float fov;
    public float nearClipPlane;
    public float farClipPlane;
    public Rect viewport;
}

public CameraSettings leftCameraSettings;
public CameraSettings rightCameraSettings;
定义一ScriptableRenderPass,在渲染Opaque之前调用。
splitScreenPass.renderPassEvent = RenderPassEvent.BeforeRenderingOpaques;
读取一组CameraSettings参数,通过CommandBuffer设置ViewMatrix、ProjectionMatrix和ViewportMatrix,再调用DrawRenderers();然后对另一个CameraSettings参数重复上述过程。
dummyCamera是为了方便完成Matrix计算。
class SplitScreenPass : ScriptableRenderPass
{
    ...
    Camera dummyCamera;

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        profilingSampler = new ProfilingSampler("Split Screen Pass");

        var drawingSettings = CreateDrawingSettings(shaderTagIdList, ref renderingData, renderingData.cameraData.defaultOpaqueSortFlags);
        var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);

        CommandBuffer cb = CommandBufferPool.Get();

        using (new ProfilingScope(cb, profilingSampler))
        {
            //left camera
            dummyCamera.transform.SetPositionAndRotation(leftCameraSettings.position, Quaternion.Euler(leftCameraSettings.rotation));
            dummyCamera.fieldOfView = leftCameraSettings.fov;
            dummyCamera.nearClipPlane = leftCameraSettings.nearClipPlane;
            dummyCamera.farClipPlane = leftCameraSettings.farClipPlane;
            dummyCamera.rect = leftCameraSettings.viewport;
            var viewMatrix = dummyCamera.worldToCameraMatrix;
            var orginPixelRect = renderingData.cameraData.camera.pixelRect;
            float aspect = (float)orginPixelRect.width / (float)orginPixelRect.height * leftCameraSettings.viewport.size.x / leftCameraSettings.viewport.size.y;
            var projMatrix = Matrix4x4.Perspective(leftCameraSettings.fov, aspect, leftCameraSettings.nearClipPlane, leftCameraSettings.farClipPlane);
            cb.SetViewProjectionMatrices(viewMatrix, projMatrix);
            cb.SetViewport(new Rect(0, 0, orginPixelRect.width * leftCameraSettings.viewport.size.x, orginPixelRect.height * leftCameraSettings.viewport.size.y));
            context.ExecuteCommandBuffer(cb);
            cb.Clear();            
            context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref filteringSettings);

            //right camera
            dummyCamera.transform.SetPositionAndRotation(rightCameraSettings.position, Quaternion.Euler(rightCameraSettings.rotation));
            dummyCamera.fieldOfView = rightCameraSettings.fov;
            dummyCamera.nearClipPlane = rightCameraSettings.nearClipPlane;
            dummyCamera.farClipPlane = rightCameraSettings.farClipPlane;
            dummyCamera.rect = rightCameraSettings.viewport;
            viewMatrix = dummyCamera.worldToCameraMatrix;
            aspect = (float)orginPixelRect.width / (float)orginPixelRect.height * rightCameraSettings.viewport.size.x / rightCameraSettings.viewport.size.y;
            projMatrix = Matrix4x4.Perspective(rightCameraSettings.fov, aspect, rightCameraSettings.nearClipPlane, rightCameraSettings.farClipPlane);
            cb.SetViewProjectionMatrices(viewMatrix, projMatrix);
            cb.SetViewport(new Rect(orginPixelRect.width * rightCameraSettings.viewport.x, 0, orginPixelRect.width * rightCameraSettings.viewport.size.x, orginPixelRect.height * rightCameraSettings.viewport.size.y));         
            context.ExecuteCommandBuffer(cb);
            cb.Clear();
            context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref filteringSettings);
        }
        
        context.ExecuteCommandBuffer(cb);
        cb.Clear();
        CommandBufferPool.Release(cb);
    }
    ...
}
新建一个URP版本的Rendering Pipeline Asset,其中的Renderer中把上面的RenderFeature加上,配置好左右CameraSettings参数。让唯一的相机使用新建的Renderer。



CameraSettings参数

效果如下:物体和阴影没问题,但右边的光照不正确。



单相机下,光照效果不正确

问题比较好定位:shader中_WorldSpaceCameraPos一直是主相机位置,没有根据左右CameraSettings进行正确设置。左边效果正确是个偶然,因为其相机position和主相机恰好一样。
修正比较简单:根据CameraSettings,重新赋值_WorldSpaceCameraPos,时机在CameraSettings设置后,DrawRenderers前。
readonly int worldSpaceCameraPos = Shader.PropertyToID("_WorldSpaceCameraPos");

public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
    //读取leftCameraSettings设置dummyCamera
    ...
    cb.SetGlobalVector(worldSpaceCameraPos, leftCameraSettings.position);
    context.ExecuteCommandBuffer(cb);
    cb.Clear();
    context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref filteringSettings);

    //读取rightCameraSettings设置dummyCamera
    ...   
    cb.SetGlobalVector(worldSpaceCameraPos, rightCameraSettings.position);
    context.ExecuteCommandBuffer(cb);
    cb.Clear();
    context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref filteringSettings);
}



单光源下,单相机分屏

性能




Frame Debug比较

单相机情况下只走一遍RenderSingleCamera(),ShadowMap、Skybox、FinalBlit仅一次。
几个问题

Culling

前面描述唯一主相机时提到过,需要让主相机的四棱台覆盖所有物体,这里展开说一下原因。
假设没有完全覆盖,意味着某些物体不在主相机的四棱台内。尝试在左右分屏中单独调用context.Cull(),是可以画出不在主相机内、在左右分屏内的物体的,但是阴影存在问题。因为ShadowMap的生成与主相机的参数有关,不在主相机内的物体不会有shadow,而上述分屏的Pass并未涉及到对ShadowMap处理。
逐物体光照

逐物体光照是一个更符合实际情况的场景,尤其在对画面要求越来越高的当下。尝试构造1方向+8点光的场景,4盏点光只照红色方块,另4盏点光只照绿色方块。
直接使用主相机cullResults时,单相机分屏的效果是正确的



不单独culling

如果根据不同CameraSettings设置,分别进行culling,光照结果是错误的。



左右分别culling的错误效果

上面场景中9盏光都在主相机和左右CameraSettings的四棱台范围内,对比他们的裁剪结果有助于定位问题。
主相机和左右分别裁剪后,cullResults.visibleLights的光源个数和顺序是一致的,这块没问题。
主光源、左、右culling得到的visibleLights均为:
Directional Light,L3,R4,R1,L2,R2,R3,L4,L1问题应该出在Unity内部的逐物体光源列表,也就是cullResults.GetLightIndexMap()指向的array:
主光源culling:-1,0,1,2,3,4,5,6,7
左culling:0,1,2,3,4,5,6,7,8
右culling:0,1,2,3,4,5,6,7,8Unity内部是怎么做的我也不知道(看Felipe的代码[1],-1估计是方向光),但这个不同确实导致了问题的出现,因为尝试修改(cullResults.SetLightIndexMap())可以获得正常显示。但也仅限于这个特殊情况,如果左右裁剪掉的光源不同时,正确的array该是怎样的就不知道了。如果有朋友对这块有了解,期待能得到一些提示。
总结

实现了单相机分屏的基础功能,性能较多相机有所改善。
但对于逐物体光照/Culling的处理存在问题,涉及到Unity的闭源实现,思路和方法不多,现在直接使用该方案会有较多限制,后续还需要再尝试和改进。
--2023.2
参考


  • ^https://gist.github.com/phi-lira/6670451882c63c0b4c8138c9fdbfa174

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2023-2-28 10:36 | 显示全部楼层
发表于 2023-2-28 10:44 | 显示全部楼层
很吊
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-6-17 07:37 , Processed in 0.154377 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表