这篇文章让我们来讲讲天空盒和水吧。
仔细想想,这些文章大概也不会有除我以外的人看了,
就写得更自由些好了。
唔姆,一这么讲突然后不知道要写什么了,
先来总结一下,这章节的主要部分吧——天空盒,平面反射和水面。
那么正篇开始咯。
天空盒部分
这里我们使用了一个从UE里扒拉来的球型的天空盒,为何么要用球型呢?大概可能是六面体的在这种环境下棱角比较明显吧。太长时间再写这些真不好,都快不记得了。
我讨厌这种模模糊糊的感觉,但估计也没心气再去看一遍了,姑且先这样。
为了这个球型,我们需要编写shader去适配。
当然在shader之前还有很重要的设置,关于天空球的uv,在ue和unity还是要做下转换的。
不是很规律呢,以后只能要用再跑回来看两眼了。
关于他的shader其实也很简单,涉及到了一个平台之间的差异化,但总而言之都是用来将o.pos.z.变小,小到足以被camera拍摄到的程度。
还是蛮重要的,不然天空就灰蒙蒙的啦,我总感觉这里当时宕机了蛮久的,不知道是天空盒还是平面反射的问题,好像重启就好,真是玄学。
以后的我看到这里,切记切记,如何感觉真没什么地方改错,那就是电脑的锅,坐板凳上几小时不值得,重启电脑性价比高得多。
平面反射部分
这是我们最没参与感到的一集了,kerry佬的脚本挂在水面上就结束了,如果不细究代码实现的话,这一步最难的部分,就是在后面shader中把反射贴图的名字取对。
对大学生太难了,不如看看代码怎么实现的。核心代码应该如下:
Camera CreateReflectionCamera(Camera cam)
{
//生成Camera
String reflName = gameObject.name + "Reflection" + cam.name;
GameObject go = new GameObject(reflName);
//go.hideFlags = HideFlags.HideAndDontSave;
go.hideFlags = HideFlags.HideAndDontSave;
Camera reflectCamera = go.AddComponent<Camera>();
//设置反射相机的参数
HoldCameraSettings(reflectCamera);
//创建RT并绑定Camera
if (!reflectCamera.targetTexture)
{
reflectCamera.targetTexture = CreateTexture(cam);
}
return reflectCamera;
}
//设置反射相机的参数
void HoldCameraSettings(Camera heplerCam)
{
heplerCam.backgroundColor = Color.black;
heplerCam.clearFlags = _reflectSkybox ? CameraClearFlags.Skybox : CameraClearFlags.SolidColor;
heplerCam.renderingPath = RenderingPath.Forward;
heplerCam.cullingMask = _reflectionMask;
heplerCam.allowMSAA = false;
heplerCam.enabled = false;
}
//创建RT
RenderTexture CreateTexture(Camera sourceCam)
{
int width = Mathf.RoundToInt(Screen.width / _downsample);
int height = Mathf.RoundToInt(Screen.height / _downsample);
RenderTextureFormat formatRT = sourceCam.allowHDR ? RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default;
RenderTexture rt = new RenderTexture(width, height, 24, formatRT);
rt.hideFlags = HideFlags.DontSave;
return rt;
}
……
void RenderReflection(Camera currentCam, Camera reflectCamera)
{
if (reflectCamera == null)
{
Debug.LogError("反射Camera无效");
return;
}
if (_sharedMaterial && !_sharedMaterial.HasProperty(_reflectionTex))
{
Debug.LogError("Shader中缺少_ReflectionTex属性");
return;
}
//保持反射相机的参数
HoldCameraSettings(reflectCamera);
if (_reflectSkybox)
{
if (currentCam.gameObject.GetComponent(typeof(Skybox)))
{
Skybox sb = (Skybox)reflectCamera.gameObject.GetComponent(typeof(Skybox));
if (!sb)
{
sb = (Skybox)reflectCamera.gameObject.AddComponent(typeof(Skybox));
}
sb.material = ((Skybox)currentCam.GetComponent(typeof(Skybox))).material;
}
}
bool isInvertCulling = GL.invertCulling;
GL.invertCulling = true;
Transform reflectiveSurface = this.transform; //waterHeight;
Vector3 eulerA = currentCam.transform.eulerAngles;
reflectCamera.transform.eulerAngles = new Vector3(-eulerA.x, eulerA.y, eulerA.z);
reflectCamera.transform.position = currentCam.transform.position;
Vector3 pos = reflectiveSurface.transform.position;
pos.y = reflectiveSurface.position.y;
Vector3 normal = reflectiveSurface.transform.up;
float d = -Vector3.Dot(normal, pos) - _clipPlaneOffset;
Vector4 reflectionPlane = new Vector4(normal.x, normal.y, normal.z, d);
Matrix4x4 reflection = Matrix4x4.zero;
reflection = CalculateReflectionMatrix(reflection, reflectionPlane);
_oldpos = currentCam.transform.position;
Vector3 newpos = reflection.MultiplyPoint(_oldpos);
reflectCamera.worldToCameraMatrix = currentCam.worldToCameraMatrix * reflection;
Vector4 clipPlane = CameraSpacePlane(reflectCamera, pos, normal, 1.0f);
Matrix4x4 projection = currentCam.projectionMatrix;
projection = CalculateObliqueMatrix(projection, clipPlane);
reflectCamera.projectionMatrix = projection;
reflectCamera.transform.position = newpos;
Vector3 euler = currentCam.transform.eulerAngles;
reflectCamera.transform.eulerAngles = new Vector3(-euler.x, euler.y, euler.z);
reflectCamera.Render();
GL.invertCulling = isInvertCulling;
}
前面几个函数就生成下摄像机这个物体和Render Target的贴图,但真正的计算包括相机位置旋转等等的计算一个都最后一个里。挺复杂的,我让大哥给我写了端解释,聊胜于无。
- 相机设置保持(Hold Camera Settings):保存当前反射相机的参数。
- Skybox设置:如果需要反射Skybox,并且主相机有Skybox组件,将反射相机的Skybox设置为与主相机相同。
- 剔除状态(Culling State):在渲染之前,将OpenGL的剔除状态设置为反转,这通常是为了正确渲染反射表面。
- 获取反射表面信息:通过获取表面的位置、法线等信息,计算用于反射的平面方程。
- 计算反射矩阵(Calculate Reflection Matrix):使用平面信息计算一个反射矩阵,该矩阵用于将相机位置和视图矩阵转换到反射的视角。
- 计算剔除平面(Camera Space Plane):计算一个用于剪切的平面,确保只有在平面上方的对象被渲染。
- 设置投影矩阵(Projection Matrix):通过将计算得到的剔除平面应用到当前相机的投影矩阵,实现视锥的剪裁。
- 设置反射相机位置和角度:将反射相机的位置和角度调整为与主相机相同,但相对于反射表面进行反转。
- 渲染反射:使用调整后的反射相机进行渲染,从而获得反射效果。
- 还原剔除状态:在渲染完成后,还原OpenGL的剔除状态。
这段代码的核心思想是通过创建一个虚拟的反射相机,使用其视角渲染反射纹理,以模拟表面的反射效果。
这次说得不太算废话,代码的逻辑大抵都是对的,虽说忽略了里面的计算,没想到我和大哥55开,不知道国产的大哥什么时候懂这些计算。
总之,放工具库里好了,会不会写是一回事,会不会用是一回事,要相信自己,要是有一天有人拿刀抵着我,我tm也就能写出来了。
Water!
终于到水了,之前觉得挺好的,没想到现在再看,竟有点油腻的感觉,感慨万千,没想到我的学习迭代竟如此之快(doge),虽然我感觉可能确实是太久没看了,加上调参不对的问题。
效果不好,至少有一小半的锅在显示屏上(一本正经),一样参数在我的屏幕上竟是如此苍白,没想到大就是好的真理,也是有极限的~
那么回归正题,来到快乐的连连看时间。
唔,不能如此着急,先分析下这里的水的组成部分吧。
掏个战力分析仪出来,等会。
一眼盯帧,纯纯的坏水,唔严肃的将,这里的水主要有反射折射,水的波动,高光,以及波光粼粼的部分组成。
下面就让我们来康康精彩刺激的连连看吧。
首先是worldnormal部分。这是造成水面有波动感的主要原因。
后面连这个连多了,感觉不用怎么记,也记下来了。
前面部分用世界坐标的xz作为采样的基础uv,记得做个Tilling,根据时间做两次偏移,乘以不同方向大小的速度向量,tilling也可以一大一小,采样完两张后,相加或者使用BlendNormal节点,此处后面的操作,由于给的法线贴图是切线空间的,所以使用xy分量自主生成出z来,再合并通过WorldNormal转到世界空间记作worldnormal.
之后我们就可以用worldnormal做个扰动的水的效果了,这里又几个注意点,首先是uv选屏幕空间下的xy,而与之不同的是我们使用worldNormal的xz做扰动,
当然在normal的处理上还是很讲究的,像是我们可以看到normal的xz还除了一大串节点后的结果,凭借我高超的记忆力,一眼看出了下面的节点是用来得到一个裁剪空间下点到摄像机的一个大概的距离(w),用来给normal的作用做个由远及近,由弱到强的效果,至于+1则是保证除数要大于1。
后面我们就来到Blink的部分。
这里的normal太久没看了,跟上面的简直一摸一样,事实上,还真就是复制的,改了几个参数名,多的就不讲了
这里也是常规,采样了reflect,但是对采样后的结果减了一个Threshold,再与0取大,乘以强度就能得到波光粼粼的一块了。
唔忘截图了,算了我心里记得就好。
然后来上高光吧。
Spec部分节点虽然多,但原理是我们最熟悉的那一部分,
上面是熟悉的NdotH,下半部分则是根据世界坐标离camera的距离做了一个mask,以防止海岸线部分的高光强烈,有种割裂感。
至于水下部分依旧是xz做uv,然后法线扰动,似乎还有个根据实现方向的PO要加,我竟然忘连进去了,多少有点开小差了。
以上各个组成部分差不多就连接好了,然后就到最后的final_color环节
我们利用Fresnel做UndergroundWater和ReflectWater之间插值的因子,这样使得我们平视水面时看到的多为反射,而往水下看是就能看到被水折射过的水底了。同时我们也对reflect做了相乘处理,防止近处的反射过于强烈。
至此,简单的水面和天空盒的效果就渲染好了。
完善一下的话,确实可以作为角色展示的界面,回头康康原神那个流沙的地面是怎么做的~
本文地址: 天空盒和简单水体渲染
您必须 登录 才能发表评论