自编渲染器Coocoo3D中的材质与渲染管线设计

这是对自编渲染器Coocoo3D的渲染管线设计的解析,交流图形编程技术。

你可以在此处找到Coocoo3D的完整源码 https://github.com/sselecirPyM/Coocoo3D

Coocoo3D的渲染管线

这是完全可编程的渲染管线,不包含任何系统预定义的对象,为快速实现原型设计,并且在性能和质量之间取得平衡。

渲染管线使用C#作为编程语言,大量使用了C#中的特性(Attribute),显著减少了编程所需的代码量。

接下来将介绍渲染管线的具体设计。

材质

Coocoo3D的材质不同于Unity,不能直接指定材质所使用的Shader。渲染时使用何种Shader由渲染管线决定。用户可以编写使用自定义着色器的渲染管线。

在渲染管线中声明字段并添加UIShowAttribute特性,可以直接在材质面板中显示。


        [UISlider(0.0f, 1.0f, UIShowType.Material, "金属")]
        public float Metallic;//此处为默认值,可在运行时修改

        [UISlider(0.0f, 1.0f, UIShowType.Material, "粗糙")]
        public float Roughness = 0.8f;//此处为默认值,可在运行时修改

        [UIDragFloat(0.01f, 0, float.MaxValue, UIShowType.Material, "发光")]
        public float Emissive;//此处为默认值,可在运行时修改

        [UISlider(0.0f, 1.0f, UIShowType.Material, "高光")]
        public float Specular = 0.5f;//此处为默认值,可在运行时修改

        [UIShow(UIShowType.Material)]
        [PureColorBaker(1, 1, 1, 1)]//为此纹理提供默认值
        [Format(ResourceFormat.R8G8B8A8_UNorm)]//为此纹理提供默认值
        [Size(32, 32)]//为此纹理提供默认值
        [Srgb]
        public Texture2D _Albedo;

资源管理

Coocoo3D的渲染资源由系统进行管理,用户代码不控制资源的创建、释放和修改。

        [AOV(AOVType.Color)]//AOV是Arbitrary Output Variables的简写
        [Size("Output")]//这个纹理的尺寸属于"Output"组,可以在每帧渲染开始前修改
        [Format(ResourceFormat.R8G8B8A8_UNorm)]//此纹理的格式
        public Texture2D output;//在尺寸、格式修改后仍然有效。

        [AOV(AOVType.Depth)]
        [Size("Output")]
        [Format(ResourceFormat.D32_Float)]
        [AutoClear]//每帧开始时清理此资源
        public Texture2D depth;

        public override void BeforeRender()
        {
            renderWrap.GetOutputSize(out int width, out int height);//RenderWrap是一个辅助操作类,此处使用它的实例。
            renderWrap.SetSize("Output", width, height);//修改"Output"组的尺寸。

        }

运行时烘焙

Coocoo3D使用特性标注的方法创建运行时烘焙任务,只要在想要烘焙的纹理成员上添加RuntimeBakeAttribute特性,系统就会在每帧开始时检查并运行烘焙。

用户可以创建自己的类型并继承自RuntimeBakeAttribute来添加自定义烘焙任务。

        [UIShow(name: "天空盒")]
        public Texture2D skyboxTexture;//输入的纹理

        [Size(1024, 1024, 6)]
        [Format(ResourceFormat.R16G16B16A16_Float)]
        [CubeFrom2D(nameof(skyboxTexture))]//此类继承自RuntimeBakeAttribute,用途是将全景图转换为TextureCube
        [BakeDependency(nameof(skyboxTexture))]//烘焙依赖项,每次重新设置天空盒纹理后依赖项失效,然后烘焙就会重新运行。
        public TextureCube _SkyBox;//天空盒

        [Size(512, 512, 6)]
        [Format(ResourceFormat.R16G16B16A16_Float)]
        [EnvironmentReflection(nameof(_SkyBox))]//此类继承自RuntimeBakeAttribute,用于烘焙IBL光照。
        [BakeDependency(nameof(_SkyBox))]//表明此项依赖于天空盒,当天空盒失效时需要重新烘焙。
        public TextureCube _Environment;//预过滤环境贴图

通过新建一个类并继承RuntimeBakeAttribute类来创建一个自动运行任务。

着色器

Coocoo3D使用原始的HLSL作为着色器语言,并且不使用代码生成以及着色器反射。Coocoo3D会在运行时编译着色器,并且可以通过宏定义的方式使用分支。何时使用何种着色器分支完全由用户编写的代码决定。

系统会自动缓存所使用的着色器,用户代码无需对此特殊处理。

传入着色器的参数由用户编写的代码控制,系统提供了辅助方法帮助自动控制或编程控制的参数传入,示例如下。


    public class ForwordRenderPass//并不是真正的pass
    {
        public DrawObjectPass drawObject = new DrawObjectPass()
        {
            shader = "PBRMaterial.hlsl",//使用的着色器
            renderTargets = new string[1],//初始化时赋值
            depthStencil = null,//初始化时赋值
            srvs = new string[] //将string[]参数传入渲染管线中,系统会通过名称自动匹配合适的资源,优先级 材质>用户定义>回退
            {
                "_Albedo",//来自材质
                "_Metallic",//来自材质
                "_Roughness",//来自材质
                "_Emissive",//来自材质
                "_ShadowMap",//来自用户定义
                "_Environment",//来自用户定义
                "_BRDFLUT",//来自用户定义
                "_Normal",//来自材质
            },
            CBVPerObject = new object[] //将object[]传入渲染管线中,系统会自动匹配参数。
            {
                null,
                "Metallic",
                "Roughness",
                "Emissive",
                "Specular",
            },
            CBVPerPass = new object[]
            {
                nameof(ViewProjection),
                nameof(View),
                nameof(CameraPosition),
                nameof(Brightness),
                nameof(Far),
                nameof(Near),
                nameof(Fov),
                nameof(AspectRatio),
                nameof(ShadowMapVP),
                nameof(ShadowMapVP1),
                nameof(LightDir),
                0,
                nameof(LightColor),
                0,
                "SkyLightMultiple",
                "FogColor",
                "FogDensity",
                "FogStartDistance",
                "FogEndDistance",
            },
        };
    }

在运行时用户代码可以调用着色器,例如DrawObjectPass

        public override void Execute(RenderWrap renderWrap)
        {
            renderWrap.SetRootSignature(rs);
            renderWrap.SetRenderTarget(renderTargets, depthStencil, clearRenderTarget, clearDepth);
            if (scissorViewport != null)
            {
                var rect = scissorViewport.Value;
                renderWrap.SetScissorRectAndViewport(rect.Left, rect.Top, rect.Right, rect.Bottom);
            }
            var desc = GetPSODesc(renderWrap, psoDesc);

            var writer = renderWrap.Writer;
            writer.Clear();
            if (CBVPerPass != null)
            {
                renderWrap.Write(CBVPerPass, writer);
                writer.SetBufferImmediately(2);//设置CBV槽位2
            }

            keywords2.Clear();//重新利用容器
            foreach (var renderable in renderWrap.MeshRenderables())
            {
                keywords2.AddRange(this.keywords);
                if (filter != null && !filter.Invoke(renderWrap, renderable, keywords2)) continue;//使用过滤器跳过不需要渲染的部分
                foreach (var keyMap in AutoKeyMap)//使用自动关键字
                {
                    if (true.Equals(renderWrap.GetIndexableValue(keyMap.Item1, renderable.material)))
                        keywords2.Add((keyMap.Item2, "1"));
                }
                if (renderable.gpuSkinning)//Coocoo3D可以使用GPU蒙皮,也可以不使用GPU蒙皮。
                {
                    keywords2.Add(new("SKINNING", "1"));
                }
                renderWrap.SetShader(shader, desc, keywords2, enableVS, enablePS, enableGS);//设置着色器,系统会自动编译所需的着色器变种。

                if (renderable.gpuSkinning)
                    renderWrap.SetCBV(renderWrap.GetBoneBuffer(), 0);

                CBVPerObject[0] = renderable.transform;

                renderWrap.Write(CBVPerObject, writer, renderable.material);
                writer.SetBufferImmediately(1);//设置CBV槽位1

                renderWrap.SetSRVs(srvs, renderable.material);

                renderWrap.Draw(renderable);
                keywords2.Clear();
            }
        }

Pass

此渲染管线设计中没有Pass的概念,需要用户自行编写代码来实现Pass。

精心设计的代码可以极大的提高可读性,可以使用极少的代码实现功能。

        public override void Render()
        {
            var camera = renderWrap.Camera;

            forwordRenderPass.SetCamera(camera);
            forwordRenderPass.Execute(renderWrap);
            postProcess.Execute(renderWrap);
        }

以上就是渲染管线设计的核心内容。

模型:Kizuna AI ©Kizuna AI, 天満宮風ステージ byムムム

前向渲染管线

此渲染管线的代码位于ForwardRenderPipeline.cs中,ForwardRenderPass.cs实现具体功能。

在ForwardRenderPass类里,有三个和渲染相关的字段:drawSkyBox、drawShadowMap、和drawObject。其中drawShadowMap和drawObject的类型是DrawObjectPass,在DrawObjectPass类里实现了遍历并渲染物体的功能。(Coocoo3D未实现剔除)

DrawObjectPass的AutoKeyMap字段允许这个pass自动的选择shader变体,如果ForwardRenderPipeline里含有被[Indexable]标记的字段并且值为true(这个值可以被材质覆盖),则会自动的为shader添加关键字。

            AutoKeyMap =
            {
                ("EnableFog","ENABLE_FOG"),
            }

渲染部分,请看ForwardRenderPass类的Execute函数,此函数记录一些渲染所需的参数,然后运行渲染。

        public void Execute(RenderWrap renderWrap)
        {
            drawSkyBox.renderTargets[0] = renderTarget;//设置pass的渲染目标
            drawObject.renderTargets[0] = renderTarget;
            drawObject.depthStencil = depthStencil;
            var dls = renderWrap.directionalLights;
            if (dls.Count > 0)//如果存在平行光,则渲染平行光照和阴影
            {
                ShadowMapVP = dls[0].GetLightingMatrix(InvertViewProjection, 0, 0.977f);
                ShadowMapVP1 = dls[0].GetLightingMatrix(InvertViewProjection, 0.977f, 0.993f);
                LightDir = dls[0].Direction;
                LightColor = dls[0].Color;
                drawObject.keywords.Add(("ENABLE_DIRECTIONAL_LIGHT", "1"));//添加关键字来使用含有光照的shader变体
            }
            else
            {
                ShadowMapVP = Matrix4x4.Identity;
                ShadowMapVP1 = Matrix4x4.Identity;
                LightDir = Vector3.UnitZ;
                LightColor = Vector3.Zero;
            }

            renderWrap.PushParameters(this);//将此类被[Indexable]标记的字段也添加到参数中。
            if (dls.Count > 0)//渲染平行光阴影
            {
                renderWrap.ClearTexture("_ShadowMap");
                var shadowMap = renderWrap.GetRenderTexture2D("_ShadowMap");
                drawShadowMap.CBVPerObject[1] = ShadowMapVP;
                drawShadowMap.scissorViewport = new Rectangle(0, 0, shadowMap.width / 2, shadowMap.height / 2);
                drawShadowMap.Execute(renderWrap);
                drawShadowMap.CBVPerObject[1] = ShadowMapVP1;
                drawShadowMap.scissorViewport = new Rectangle(shadowMap.width / 2, 0, shadowMap.width / 2, shadowMap.height / 2);
                drawShadowMap.Execute(renderWrap);
            }
            drawSkyBox.Execute(renderWrap);//渲染天空盒
            drawObject.Execute(renderWrap);//渲染物体
            renderWrap.PopParameters();//移除参数覆盖

            drawObject.keywords.Clear();//清除之前添加的关键字
        }
posted @ 2022-04-30 17:33  sselecirpym  阅读(404)  评论(0)    收藏  举报