Unity 2D Light (2) - 渲染
光与阴影的渲染
光与阴影的渲染需要两类贴图,LightMap(光照贴图) 和 ShadowMap(阴影贴图)。
每个光源都需要渲染一张LightMap、ShadowMap。LightMap 是光渲染结果,ShadowMap 是阴影渲染结果。
两者可以同一次渲染,分开渲染是为了之后做软阴影或半影。
这里目前仅简单的混合叠加光源,所以将所有光源结果渲染到一张 LightMap 里。
Light Mesh
目前生成的LightMesh并不包含物体模型,生成的阴影贴图便将物体也包括进去,而光照贴图不包括物体。
将光接触的物体加入Mesh,结果:
但是会导致光部分接触到的物体整体都被渲染,应该仅有光照到地方被渲染。
需要进行分割。
通用的分割不谈(应该是用的三角剖分Delaunay),一开始我想利用排序好的端点做分割(未做复杂外形的覆盖测试),原理是:
- 由于端点是排好序的,所以可以获取物体扫描的第一个端点和最后一个端点。
- 三角化的时候保存一个物体的所有射线弧度(ShadowMesh一个三角网格通过开始弧度、结束弧度和一个线段三角化)
- 射线分端点射线和分割射线,端点射线一般去除即可,分割射线顶点替换成其击穿物体与物体第二条线段相交的顶点。
- 之后分以下几种情况:
- 两条射线弧度同开始端点和结束端点弧度,则渲染整个物体
- 两条射线仅一个同开始或结束端点,分割的那条射线所获取的顶点替换成射线穿过物体与物体第二个线段的相交点。再将物体大于开始射线和分割射线的所有端点加入三角化的输出点。
3. 多条射线仅一个同开始或结束端点,其他射线未端点射线,分割的那条射线所获取的顶点替换成射线穿过物体与物体第二个线段的相交点。再将物体大于开始射线和分割射线的所有端点加入三角化的输出点。去除所有中间射线的顶点。
4. 两个射线都不同开始或结束端点(中空),两个分割射线所获取的顶点替换成射线穿过物体与物体第二个线段的相交点。
- 多个射线,由于只有成对的射线才能构成一个三角网格,所以两两判断其是1-4的那种情况,然后分别分割。
这个暂用方案并不好用,需要考虑的状况比较多(目前还不兼容拼接的物体),而且使用到了排序端点、线段数据,还需要不停的修改扫描后的三角网格顶点数据,简而言之就是耦合度太高了。
分割物体方法静态光源还好,有优化的余地,如果是动态光源,光源数量一多实时分割的话计算量有点大了。
选择不分割,如果没什么受光面高光之类的效果,实际渲染效果其实也可以接受。
选择将物体加入LightMesh,是为了之后进行光着色。如果不需要物体受光影响或者通过其他途径着色(比如仅受环境光影响),这部分可以省去。
贴图绘制
我使用的是Unity2019版本,绘制贴图使用的是CommandBuffer。旧版本可以使用Graphics.DrawTexture或者GL。
ShadowMap
使用CommandBuffer.DrawMesh,将ShadowMesh绘制到目标渲染贴图。因为暂时不做半影,所以使用的着色器很简单,颜色置1,之后加个高斯模糊即可。深度之类的看情况而定。
LightMap
需要采样 ShadowMap, 如果不做软阴影,阴影贴图生成、采样混合的步骤可以省去(光直接使用使用lightMesh)。如果需要软阴影或者之后的半影,那么每个光源都需要一个正方形的Mesh绘制光源,然后采样阴影混合(一般相乘即可)生成LightMap。
光的衰减
一般来说都是使用CookieTexture。
- 通过程序
圆的面积是 πr²,光的强度为 1 / (πr²) 简化为 1/d² ,d为当前位置到光源的距离,为防止d接近与0时结果无限大,最终结果为 1 / (1 + d²)。
// shader frag
float3 lightVec = i.worldPos - _Light2DPos;
float rangeFactor = (_LightMaxRange - length(lightVec))/_LightMaxRange;
float atten = 1 / (1 + dot(lightVec,lightVec));
col.rgb = atten*rangeFactor*_LightColor*_Intensity;
这是个简化的算法,比较真实的渲染方程参考:用 C 语言画光
同大多数程序式贴图一样(Procedural Texture)好处是可以通过代码动态修改。
- 通过CookieTexture
点光源CookieTexture:
使用Cookie贴图需要构建ShadowMesh时正确设定uvs。
好处是比较方便,构建复杂外形光源只需要更换CookieTexture就可以了。
光的混合
光的混合直接叠加即可 Blend One One
。
物体着色
不定。我这边是LightMap采样光亮后,物体颜色与强度相乘(光的强度:rgb转换成hsv取v值或者rgb的模估算),然后与光的颜色相加。
示例
// c#
MaterialPropertyBlock matPropBlock = new MaterialPropertyBlock();
bool first = true;
lightMapCmdBuffer.Clear();
lightMapCmdBuffer.GetTemporaryRT(shadowMapTmp, cam.pixelWidth, cam.pixelHeight, 0, FilterMode.Bilinear, RenderTextureFormat.ARGBFloat);
foreach (var light in lights)
{
if (light.lightMesh != null)
{
var trs = Matrix4x4.TRS(light.transform.position, light.transform.rotation, light.transform.localScale);
// render shadow map
lightMapCmdBuffer.SetRenderTarget(shadowMapTex);
lightMapCmdBuffer.ClearRenderTarget(true, true, Color.black);
lightMapCmdBuffer.DrawMesh(light.lightMesh, trs, shadowMat);
blurMat.SetFloat("_BlurDownSample", blurDownSample);
lightMapCmdBuffer.Blit(shadowMapTex, shadowMapTmp, blurMat);
lightMapCmdBuffer.Blit(shadowMapTmp, shadowMapTex, blurMat);
// render light map
lightMapCmdBuffer.SetRenderTarget(lightMapTex);
if (first)
{
first = false;
lightMapCmdBuffer.ClearRenderTarget(true, true, Color.black);
}
matPropBlock.SetFloat("_Intensity", light.intensity);
matPropBlock.SetVector("_Light2DPos", light.transform.position);
matPropBlock.SetTexture("_ShadowMap", shadowMapTex); // 阴影采样
matPropBlock.SetColor("_LightColor", light.color);
matPropBlock.SetTexture("_LightCookie", light.cookieTexture);
matPropBlock.SetFloat("_LightMaxRange", light.range);
// 根据光源的范围生成一张正方形Mesh
// 如果不实现软阴影,将_ShadowMap的采样过程去掉,然后将light.GetQuadMesh() 改为 light.lightMesh
lightMapCmdBuffer.DrawMesh(light.GetQuadMesh(), trs, lightMat, 0, 0, matPropBlock);
}
}
lightMapCmdBuffer.ReleaseTemporaryRT(shadowMapTmp);
// light shader
Blend One One
vert {
o.screenPos = ComputeScreenPos(o.vertex);
}
frag {
fixed4 col = 1;
col.rgb = tex2D(_LightCookie, i.uv).r*_LightColor*_Intensity;
float shadow = tex2D(_ShadowMap, UNITY_PROJ_COORD(i.screenPos)).r;
col.rgb = col.rgb * shadow.r;
return col;
}