simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

Unity:UniversalRP de ToonMasterNode(v7.1.2)

いろいろ頑張った結果公開することに

github.com

以下は実装時に記録したメモです。
絵を作るまでにほんといろいろ調べたのですが、基本 PBR のものまねなので、深く理解できない部分が多いです
断片的なメモになりますね。

個人ブログなのでゆるして

ToonMasterNode.cs

        public const string ShadeSlotName = "Shade";
        public const string ShadeShiftSlotName = "ShadeShift";
        public const string ShadeToonySlotName = "ShadeToony";
        public const string OutlineWidthSlotName = "OutlineWidth";
        public const string ToonyLightingSlotName = "ToonyLighting";
        public const string SphereAddSlotName = "SphereAdd";
        public const string OutlineColorSlotName = "OutlineColor";

        public const int ShadeSlotId = 12;
        public const int ShadeShiftSlotId = 13;
        public const int ShadeToonySlotId = 14;
        public const int OutlineWidthSlotId = 15;
        public const int ToonyLightingSlotId = 16;
        public const int SphereAddSlotId = 17;
        public const int OutlineColorSlotId = 18;
            name = "Toon Master";
            AddSlot(new PositionMaterialSlot(PositionSlotId, PositionName, PositionName, CoordinateSpace.Object, ShaderStageCapability.Vertex));
            AddSlot(new NormalMaterialSlot(VertNormalSlotId, NormalName, NormalName, CoordinateSpace.Object, ShaderStageCapability.Vertex));
            AddSlot(new TangentMaterialSlot(VertTangentSlotId, TangentName, TangentName, CoordinateSpace.Object, ShaderStageCapability.Vertex));
            AddSlot(new ColorRGBMaterialSlot(AlbedoSlotId, AlbedoSlotName, AlbedoSlotName, SlotType.Input, Color.grey.gamma, ColorMode.Default, ShaderStageCapability.Fragment));
            AddSlot(new Vector1MaterialSlot(AlphaSlotId, AlphaSlotName, AlphaSlotName, SlotType.Input, 1f, ShaderStageCapability.Fragment));
            AddSlot(new ColorRGBMaterialSlot(ShadeSlotId, ShadeSlotName, ShadeSlotName, SlotType.Input, Color.gray, ColorMode.Default, ShaderStageCapability.Fragment));
            AddSlot(new Vector1MaterialSlot(ShadeShiftSlotId, ShadeShiftSlotName, ShadeShiftSlotName, SlotType.Input, 0.5f, ShaderStageCapability.Fragment));
            AddSlot(new Vector1MaterialSlot(ShadeToonySlotId, ShadeToonySlotName, ShadeToonySlotName, SlotType.Input, 0.8f, ShaderStageCapability.Fragment));
            AddSlot(new NormalMaterialSlot(NormalSlotId, NormalSlotName, NormalSlotName, CoordinateSpace.Tangent, ShaderStageCapability.Fragment));
            AddSlot(new ColorRGBMaterialSlot(SphereAddSlotId, SphereAddSlotName, SphereAddSlotName, SlotType.Input, Color.black, ColorMode.Default, ShaderStageCapability.Fragment));
            AddSlot(new ColorRGBMaterialSlot(EmissionSlotId, EmissionSlotName, EmissionSlotName, SlotType.Input, Color.black, ColorMode.Default, ShaderStageCapability.Fragment));
            AddSlot(new Vector1MaterialSlot(AlphaThresholdSlotId, AlphaClipThresholdSlotName, AlphaClipThresholdSlotName, SlotType.Input, 0.5f, ShaderStageCapability.Fragment));
            AddSlot(new Vector1MaterialSlot(OutlineWidthSlotId, OutlineWidthSlotName, OutlineWidthSlotName, SlotType.Input, 1f, ShaderStageCapability.Vertex));
            AddSlot(new Vector1MaterialSlot(ToonyLightingSlotId, ToonyLightingSlotName, ToonyLightingSlotName, SlotType.Input, 1f, ShaderStageCapability.Fragment));
            if (model == Model.Metallic)
                AddSlot(new Vector1MaterialSlot(MetallicSlotId, MetallicSlotName, MetallicSlotName, SlotType.Input, 0, ShaderStageCapability.Fragment));
            else
                AddSlot(new ColorRGBMaterialSlot(SpecularSlotId, SpecularSlotName, SpecularSlotName, SlotType.Input, Color.grey, ColorMode.Default, ShaderStageCapability.Fragment));
            AddSlot(new Vector1MaterialSlot(SmoothnessSlotId, SmoothnessSlotName, SmoothnessSlotName, SlotType.Input, 0.5f, ShaderStageCapability.Fragment));
            AddSlot(new Vector1MaterialSlot(OcclusionSlotId, OcclusionSlotName, OcclusionSlotName, SlotType.Input, 1f, ShaderStageCapability.Fragment));
            AddSlot(new ColorRGBMaterialSlot(OutlineColorSlotId, OutlineColorSlotName, OutlineColorSlotName, SlotType.Input, Color.black, ColorMode.Default, ShaderStageCapability.Fragment));

            // clear out slot names that do not match the slots
            // we support
            RemoveSlotsNameNotMatching(
                new[]
            {
                PositionSlotId,
                VertNormalSlotId,
                VertTangentSlotId,
                AlbedoSlotId,
                ShadeSlotId,
                ShadeShiftSlotId,
                ShadeToonySlotId,
                NormalSlotId,
                EmissionSlotId,
                AlphaSlotId,
                AlphaThresholdSlotId,
                OutlineWidthSlotId,
                ToonyLightingSlotId,
                SphereAddSlotId,
                model == Model.Metallic ? MetallicSlotId : SpecularSlotId,
                SmoothnessSlotId,
                OcclusionSlotId,
                OutlineColorSlotId,
            }, true);

f:id:simplestar_tech:20191008235030p:plain
p

UniversalToonSubShader.cs

            vertexPorts = new List<int>()
            {
                ToonMasterNode.PositionSlotId,
                ToonMasterNode.VertNormalSlotId,
                ToonMasterNode.VertTangentSlotId,
                ToonMasterNode.OutlineWidthSlotId
            },
            pixelPorts = new List<int>
            {
                ToonMasterNode.AlbedoSlotId,
                ToonMasterNode.NormalSlotId,
                ToonMasterNode.EmissionSlotId,
                ToonMasterNode.MetallicSlotId,
                ToonMasterNode.SpecularSlotId,
                ToonMasterNode.SmoothnessSlotId,
                ToonMasterNode.OcclusionSlotId,
                ToonMasterNode.AlphaSlotId,
                ToonMasterNode.AlphaThresholdSlotId,
                ToonMasterNode.ShadeSlotId,
                ToonMasterNode.ShadeShiftSlotId,
                ToonMasterNode.ShadeToonySlotId,
                ToonMasterNode.ToonyLightingSlotId,
                ToonMasterNode.SphereAddSlotId,
                ToonMasterNode.OutlineColorSlotId
            },

Lighting.hlsl

// for Toon Shading

// following code can be taken from https://github.com/Santarh/MToon
/*
MIT License

Copyright (c) 2018 Masataka SUMI

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

half ToonyIntensity(half3 lightDir, half3 normal, half shadeShift, half shadeToony)
{
    half lightIntensity = dot(normal, lightDir);
    half maxIntensityThreshold = lerp(1, shadeShift, shadeToony);
    half minIntensityThreshold = shadeShift;
    const half EPS_COL = 0.00001;
    lightIntensity = saturate((lightIntensity - minIntensityThreshold) / max(EPS_COL, (maxIntensityThreshold - minIntensityThreshold)));
    return lightIntensity;
}

// following code can be taken from https://github.com/you-ri/LiliumToonGraph
/*
MIT License

Copyright (c) 2019 You-Ri

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

half4 UniversalFragmentToon(InputData inputData, half3 albedo, half3 shade, half metallic, half3 specular,
	half smoothness, half occlusion, half3 emission, half alpha, half shadeShift, half shadeToony, half3 sphereAdd, half toonyLighting)
{
	BRDFData brdfData;
	InitializeBRDFData(albedo, metallic, specular, smoothness, alpha, brdfData);

	Light mainLight = GetMainLight(inputData.shadowCoord);
	MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI, half4(0, 0, 0, 0));

	half lighing = ToonyIntensity(mainLight.direction, inputData.normalWS, shadeShift, shadeToony) * mainLight.shadowAttenuation;
	half3 attenuatedLightColor = mainLight.color * mainLight.distanceAttenuation;
	half3 color = (inputData.bakedGI + attenuatedLightColor) * lerp(shade, albedo, lighing) * toonyLighting;

#ifdef _ADDITIONAL_LIGHTS
	uint pixelLightCount = GetAdditionalLightsCount();
	for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex)
	{
		Light light = GetAdditionalLight(lightIndex, inputData.positionWS);
		color += LightingPhysicallyBased(brdfData, light, inputData.normalWS, inputData.viewDirectionWS);
	}
#endif

#ifdef _ADDITIONAL_LIGHTS_VERTEX
	color += inputData.vertexLighting * brdfData.diffuse;
#endif
	color += sphereAdd * toonyLighting;
	color += emission * toonyLighting;
	return half4(color, alpha);
}

inline float3 TransformViewToProjection(float3 v) {
    return mul((float3x3)UNITY_MATRIX_P, v);
}

// called by PASS scripts to draw outline
float4 TransformOutlineToHClipScreenSpace(float3 position, float3 normal, float outlineWidth)
{
    half _OutlineScaledMaxDistance = 10;
    float4 nearUpperRight = mul(unity_CameraInvProjection, float4(1, 1, UNITY_NEAR_CLIP_VALUE, _ProjectionParams.y));
    float aspect = abs(nearUpperRight.y / nearUpperRight.x);
    float4 vertex = TransformObjectToHClip(position);
    float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, normal.xyz);
    float3 clipNormal = TransformViewToProjection(viewNormal.xyz);
    float2 projectedNormal = normalize(clipNormal.xy);
    projectedNormal.x *= aspect;
    vertex.xy += 0.01 * outlineWidth * projectedNormal.xy;
    return vertex;
}

ToonForwardPass.hlsl

    half4 color = UniversalFragmentToon(
		inputData,
		surfaceDescription.Albedo,
		surfaceDescription.Shade,
		metallic,
		specular,
		surfaceDescription.Smoothness,
		surfaceDescription.Occlusion,
		surfaceDescription.Emission,
		surfaceDescription.Alpha,
		surfaceDescription.ShadeShift,
		surfaceDescription.ShadeToony,
		surfaceDescription.SphereAdd,
		surfaceDescription.ToonyLighting
	);

あとは、アウトラインを作るシェーダーを頑張らねば
最終的に DrawObjectPass に
m_ShaderTagIdList.Add(new ShaderTagId("UniversalForwardOutline"));
という TagId を挿入し、次のシェーダーを走らせることになった

/*
MIT License

Copyright (c) 2019 simplestargame

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
PackedVaryings vert(Attributes input)
{
    VertexDescriptionInputs vertexDescriptionInputs;
    ZERO_INITIALIZE(VertexDescriptionInputs, vertexDescriptionInputs);
    VertexDescription vertexDescription = VertexDescriptionFunction(vertexDescriptionInputs);
    Varyings output = (Varyings)0;
    output.positionCS = TransformOutlineToHClipScreenSpace(input.positionOS, input.normalOS, vertexDescription.OutlineWidth);
    PackedVaryings packedOutput = (PackedVaryings)0;
    packedOutput = PackVaryings(output);
    return packedOutput;
}

half4 frag(PackedVaryings packedInput) : SV_TARGET 
{    
    Varyings unpacked = UnpackVaryings(packedInput);
    UNITY_SETUP_INSTANCE_ID(unpacked);
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(unpacked);

    SurfaceDescriptionInputs surfaceDescriptionInputs = BuildSurfaceDescriptionInputs(unpacked);
    SurfaceDescription surfaceDescription = SurfaceDescriptionFunction(surfaceDescriptionInputs);

    #if _AlphaClip
        clip(surfaceDescription.Alpha - surfaceDescription.AlphaClipThreshold);
    #endif

    half4 color = half4(surfaceDescription.OutlineColor, surfaceDescription.Alpha);
    return color;
}

パスは次のように書けばいいことがわかった

ShaderPass m_2DPass = new ShaderPass()
        {
            // Definition
            displayName = "Universal Forward Outline",
            referenceName = "SHADERPASS_UNLIT",
            lightMode = "UniversalForwardOutline",
            passInclude = "Packages/com.unity.render-pipelines.universal/Editor/ShaderGraph/Includes/Toon2DPass.hlsl",
            varyingsInclude = "Packages/com.unity.render-pipelines.universal/Editor/ShaderGraph/Includes/Varyings.hlsl",

            // Port mask
            vertexPorts = new List<int>()
            {
                ToonMasterNode.PositionSlotId,
                ToonMasterNode.VertNormalSlotId,
                ToonMasterNode.VertTangentSlotId,
                ToonMasterNode.OutlineWidthSlotId
            },

            pixelPorts = new List<int>
            {
                ToonMasterNode.AlbedoSlotId,
                ToonMasterNode.AlphaSlotId,
                ToonMasterNode.AlphaThresholdSlotId,
                ToonMasterNode.OutlineColorSlotId
            },
            
            // Render State Overrides
            CullOverride = "Cull Front",
            ZWriteOverride = "ZWrite On",
            ZTestOverride = "ZTest Less",

            // Required fields
            requiredAttributes = new List<string>(),

            // Required fields
            requiredVaryings = new List<string>(),

            // Pass setup
            includes = new List<string>()
            {
                "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl",
            },

            pragmas = new List<string>(),

            keywords = new KeywordDescriptor[] { },