いろいろ頑張った結果公開することに
以下は実装時に記録したメモです。
絵を作るまでにほんといろいろ調べたのですが、基本 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);
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[] { },