《塞尔达-荒野之息》中角色受多个点光源影响的做法
收集多个点光源的坐标和颜色等信息传递给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"
}