《塞尔达-荒野之息》中角色受多个点光源影响的做法

Last modified date

收集多个点光源的坐标和颜色等信息传递给Shader,在Shader中用循环函数计算出点光源的光照。

先看看《荒野之息》的实际效果:

图中1,2,3处的点光源都会对主角和NPC产生影响。

《荒野之息》可以支持很多点光源。但Unity的光照系统只支持4盏动态点光源,所以不能用Unity的点光源,必须自己另外实现一套。

因为是用点卡通渲染,不追求真实光照,所以可以用讨巧的方式。做法就是用代码将多个点光源相对于物体的角度,距离和颜色传递到shader里。然后在shader中做循环运算。

我一开始尝试的做法是:先在C#里收集角色坐标和所有点光源的坐标和颜色信息,然后计算粗出点光源相对于角色的角度“LightDir”和他们之间的距离,最后把这些参数一起传递给Shader。

但是这个做法有个问题:如果先在代码里计算了灯光角度,那么shader得到的“LightDir”只能是灯光相当于主角的角度。但是我们需要其他角色也能受到灯光影响,所以灯光角度必须要放到shader里去计算。代码层面只要把灯光坐标传递给shader就行了。

那么就先写一个灯光收集容器。

点光源上放一个标签,用来标记一些额外信息:

  • shakeStrength:光源的抖动系数
  • litSorting:光源的排序,用来控制多个点光源叠加时,谁覆盖在最上层。

代码参考:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using WalkingFat;

namespace WalkingFat
{
    public class MultiLitsTag : MonoBehaviour
    {
        public float shakeStrength = 0.5f;
        public int litSorting = 0;
    }
}

多点光源容器,可以把场景里所有点光源都填进去。然后利用标签上的“litSorting”把需要叠在最上层的灯光排序在最后。

最后输出给shader的是以下几个信息:

  • (float4)_LitPosList:xyz是点光源的世界坐标,w是灯光抖动系数,用来模拟火焰跳动效果。
  • (float4)_LitColList:xyz是点光源的颜色,w是光源的影响范围。

代码参考:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using WalkingFat;

namespace WalkingFat {
    public class MultiLitsContainer : MonoBehaviour
    {
        public Transform[] pointLits;
        private Light[] pointLightList;
        private MultiLitsTag[] lightTagList;
        //public float effectRadius = 5.0f;
        public Transform hero;
        // Start is called before the first frame update
        void Start()
        {
            pointLightList = new Light[pointLits.Length];
            lightTagList = new MultiLitsTag[pointLits.Length];
            for (int n = 0; n < pointLightList.Length; n++)
            {
                pointLightList[n] = pointLits[n].GetComponent<Light>() as Light;
                lightTagList[n] = pointLits[n].GetComponent<MultiLitsTag>() as MultiLitsTag;
            }
        }
        // Update is called once per frame
        void FixedUpdate()
        {
            Vector4[] litPosList = new Vector4[10];
            Vector4[] litColList = new Vector4[10];
            for (int n = 0; n < pointLits.Length; n++)
            {
                litPosList[n] = new Vector4(pointLits[n].position.x, 
                                                   pointLits[n].position.y, 
                                                   pointLits[n].position.z,
                                                   lightTagList[n].shakeStrength);
                litColList[n] = new Color(pointLightList[n].color.r,
                                          pointLightList[n].color.g,
                                          pointLightList[n].color.b,
                                          pointLightList[n].range);
            }
            Shader.SetGlobalFloat("_LitCount", pointLits.Length);
            Shader.SetGlobalVectorArray("_LitPosList", litPosList);
            Shader.SetGlobalVectorArray("_LitColList", litColList);
            //print("litCount = " + litCount);
        }
    }
}

然后写Shader

Shader中的就利用Normal dot LightDir的方式算出每个点光源的光照影响,并且通过之前传进来的距离参数来控制光照强弱。然后因为我们是做卡通渲染,就加个Cel的效果。

颜色就用lerp的方式一层层叠上去,我这个做法比较粗糙。可以利用之前的灯光排序让暖光源覆盖冷光源,看上去会比较舒服。

Fragment代码参考:

for (int n = 0; n < _LitCount; n++) {
    // _LitDirList[n].xyz = point light direction. _LitDirList[n].w = point light Distance.
    fixed newlitValue = max(0, dot(i.normal, normalize(_LitDirList[n].xyz + shakeOffset * 0.07))) * _LitDirList[n].w > 0.3;
    // get new light color
    fixed4 newlitCol = newlitValue * _LitColList[n];
    // add to final light color. "newlitCol" will cover old colors.
    pointLitCol = lerp(pointLitCol, newlitCol, newlitValue);
}

如果点光源是火把的话,还可以在shader里给点光源加个抖动效果,模拟火焰跳动的光照效果。做法就是用三角函数sin和cos给光源方向加点偏移。

Fragment代码参考:

// Get fire shake offset
float3 shakeOffset = float3(0,0,0);
shakeOffset.x = sin(_Time.z * 15);
shakeOffset.y = sin(_Time.z * 13 + 5);
shakeOffset.z = cos(_Time.z * 1sss2 + 7);

for (int n = 0; n < _LitCount; n++) {
    // Add “shakeOffset * 0.07” to “_LitDirList[n].xyz"
    fixed newlitValue = max(0, dot(i.normal, normalize(_LitDirList[n].xyz + shakeOffset * 0.07))) * _LitDirList[n].w > 0.3;
    fixed4 newlitCol = newlitValue * _LitColList[n];
    pointLitCol = lerp(pointLitCol, newlitCol, newlitValue);
}

最后看看效果:

shader代码参考:

Shader "WalkingFat/MultiLights/ToonMultiLits"
{
    Properties
    {
        _Tint ("Tint", Color) = (1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
        _UnlitColor ("Shadow Color", Color) = (0.5,0.5,0.5,1)
        _UnlitThreshold ("Shadow Range", Range(0,1)) = 0.1
        
        _ToonCube ("Toon Cube", Cube) = "" {} // cube map for toon light

        _RimColor("Rim Color", Color) = (0.5,0.5,0.5,1)
        _RimIntensity("Rim Intensity", Range(0.0, 100)) = 5.0
        _RimLightSampler("Rim Sampler", 2D) = "white"{} 
        
        _MultiLitFadeDistance("MultiLights Fade Distance", Float) = 20
    }

    SubShader {
        Tags { "Queue" = "Geometry" "RenderType" = "Opaque"}
        LOD 100
     
        Pass {
         
            Tags { "LightMode" = "ForwardBase" }
         
            CGPROGRAM
 
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
 
            // shadow
            #pragma multi_compile_fwdbase
            #include "AutoLight.cginc"
            #include "UnityLightingCommon.cginc" // for _LightColor0

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _Tint;
            float4 _UnlitColor;
            float _UnlitThreshold;
            samplerCUBE _ToonCube;
            float4 _RimColor;
            float _RimIntensity;
            sampler2D _RimLightSampler;
            float _MultiLitFadeDistance;
            
            // point light positions
            float _LitCount; // count must <= 10
            float4 _LitPosList[10]; // xyz = point light direction; w = shake strength;
            float4 _LitColList[10]; // xyz = light color.rgb; w = point light range;

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 posWorld : TEXCOORD0;
                float3 normal : TEXCOORD1;
                float2 uv : TEXCOORD2;
                float3 eyeDir: TEXCOORD3;
                float3 lightDir: TEXCOORD4; 
                float3 cubenormal : TEXCOORD5;
                float4 vertexPos : TEXCOORD6;
                LIGHTING_COORDS(7,8)
            };
 
 
            v2f vert(appdata v) {
                v2f o;
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.pos = UnityObjectToClipPos (v.vertex);
                o.posWorld = mul(unity_ObjectToWorld, v.vertex);
                o.normal = UnityObjectToWorldNormal(v.normal);//normalize(mul(float4(v.normal, 0.0), unity_WorldToObject).xyz);
                o.eyeDir.xyz = normalize( _WorldSpaceCameraPos.xyz - o.posWorld.xyz ).xyz;
                o.lightDir = WorldSpaceLightDir( v.vertex );
                o.cubenormal = mul (UNITY_MATRIX_MV, float4(v.normal,0));  // get cube normal data  
                o.vertexPos = mul(unity_ObjectToWorld, v.vertex);
                
                
                TRANSFER_VERTEX_TO_FRAGMENT(o);
                 
                return o;
            }

            fixed4 frag(v2f i) : COLOR {
                fixed4 col = tex2D(_MainTex, i.uv) * _Tint;
                
                // sample the texture
                fixed4 pointLitCol = fixed4(0,0,0,0);
                fixed pointLit = 0;
                
                // add fire shake effect
                float3 shakeOffset = float3(0,0,0);
                shakeOffset.x = sin(_Time.z * 15);
                shakeOffset.y = sin(_Time.z * 13 + 5);
                shakeOffset.z = cos(_Time.z * 12 + 7);
                
                for (int n = 0; n < _LitCount; n++) {
                    float litDist = distance(_LitPosList[n].xyz, i.vertexPos.xyz);
                    // Get distance between point lights and camera for fade out multi-lights effect by distance.
                    float viewDist = distance(_LitPosList[n].xyz, _WorldSpaceCameraPos.xyz);
                    float viewFade = 1 - saturate(viewDist / _MultiLitFadeDistance);
                    // Get multi light colors
                    if (litDist < _MultiLitFadeDistance) 
                    {
                        // Get light direction and distance
                        float3 litDir = _LitPosList[n].xyz - i.vertexPos.xyz;
                        float litDist = distance(_LitPosList[n].xyz, i.vertexPos.xyz);
                        // Add “shakeOffset * 0.07” to “_LitPosList[n].xyz"
                        litDir += shakeOffset * 0.07 * _LitPosList[n].w;
                        // Get light value
                        litDir = normalize(litDir);
                        fixed newlitValue = max(0, dot(i.normal, litDir)) * (_LitColList[n].w - litDist) * viewFade > 0.3;
                        // get new light color
                        fixed4 newlitCol = newlitValue * fixed4(_LitColList[n].xyz, 1);
                        // add to final light color. "newlitCol" will cover old colors.
                        pointLitCol = lerp(pointLitCol, newlitCol, newlitValue);
                    }
                }
                

                // light and shadow
                float3 normalDirection = normalize(i.normal);
                float3 lightDirection;
                fixed3 lightColor;

                float attenuation = LIGHT_ATTENUATION(i);

                lightDirection = normalize(_WorldSpaceLightPos0).xyz;
                lightColor = _Tint.rgb * _UnlitColor.rgb * _LightColor0.rgb;
                
                if (attenuation * max(0.0, dot(normalDirection, lightDirection)) >= _UnlitThreshold) {
                    lightColor = _LightColor0.rgb * _Tint.rgb; // lit fragment color
                }

                // Rimlight
                float normalDotEye = dot( i.normal, i.eyeDir.xyz );
                float falloffU = clamp( 1.0 - abs( normalDotEye ), 0.02, 0.98 );

                float rimlightDot = saturate( 0.5 * ( dot( i.normal, i.lightDir + float3(-1,0,0) ) + 1.5 ) );
                falloffU = saturate( rimlightDot * falloffU );
                falloffU = tex2D( _RimLightSampler, float2( falloffU, 0.25f ) ).r;
                float3 rimCol = falloffU * col * _RimColor * _RimIntensity;

                return float4(col.rgb * (lightColor.rgb + pointLitCol) + rimCol, 1.0);
            }
 
            ENDCG
        }
    }
    Fallback "VertexLit"
}

Share

This site is protected by wp-copyrightpro.com