|
1.0 - 概论
Lowpoly风格水体 我之前做过两次,一个是基于 Unity built-in 渲染管道的,现已经开源在Github 点这里 。
还有一个是基于 Unity LWRP 渲染管道的,这个源代码就不放上来了, 因为功能叠的太多而且写的很烂,API 过时也懒得改了...
现在这里这个是基于 Unity URP 渲染管道,作为 Lowpoly游戏制作指南 的一部分,这是份实用向的指南,希望能帮助到大家吧~噢耶! ヾ( ̄▽ ̄)~
(文章还在施工中,我会尽快补完)
能力有限多多包涵,先看看最终效果吧~!
BlinnPhong 光照 + 简单浮力 + 水面反射
水底折射 + 焦散 + 透明物体支持
船体内部裁剪 + 边缘泡沫
无缝的水底切换 + 水底雾
背面光照 + 水底雾
海域
实现以上功能需要了解 :
UnityUnity ShaderUnity URPC#一点点 三维模型知识 和 图形学知识
原理也不难,我能写代码就尽量写代码吧~ ╰( ̄▽ ̄)╮
Talk is cheap. Show me the code.
1.1- 分析和实现
大家都知道 Lowpoly风格 模型通常显得块状且简单,由多个三角形组合,二维表现是这样 ↓
Lowpoly 风格 二维
我们要实现的水体稍微高级一些,是用 Shader(着色器)制作。水面是有波浪的,随着 相机 或 光源 角度位置的变化,水面呈现出不同颜色,像下面这张动图这样~
日出日落
像波浪一般用的是 Gerstner wave算法,这算法可以计算出水面的 坐标 和 法线,但因为是 Lowpoly 风格水面高度由顶点决定,所以 Gerstner wave的法线计算公式就不适用了,我们需要在运行时为每个三角面重新计算法线,以达到下面图片的效果~
Lowpoly风格水体
一般水体(来自crest)
所以,Lowpoly风格的水体 和 其它一般的水体 实现最重要的区别就是在 顶点处理阶段。
进行下一步之前,希望你已经了解 :
Unity Manual : 编写着色器(中文)
Universal Render Pipeline
本教程使用的软件为 Unity2019.4 + Universal RP 7.53, 完整的工程文件在网盘“LowpolyWater”目录下,可以在这里下载:
链接:https://pan.baidu.com/s/1oyO_1Yr2Fr9f3xCBjb8KLw
提取码:9864
2.0 - 着色器顶点阶段
在顶点阶段计算的有 波浪 和 法线。
这是这部分最终实现的效果,场景在 “Examples1”目录下。
简单光照的Lowpoly风格水体
2.1 - 顶点偏移计算
波浪模拟
可以在这里了解各种浪 :《GPU Games》Chapter 1. Effective Water Simulation from Physical Models
这文章已经总结的很好了,我这里用的是 Gerstner wave,大部分水体都在使用的算法,因为我们在顶点计算,所以写个方法计算 世界坐标对应的波浪偏移量 :
float3 CalculateGerstnerWaveOffset(float3 positionWS, half amplitude, half length, half speed, half angle, int partCount)
{
half radian = angle * PI / 180.0;
half2 direction = half2(sin(radian), cos(radian));
half w = sqrt(2.0 * PI * 9.81f / length);
half qi = 1.0 / (amplitude * w * partCount);
half time = _Time.y;
half phase = time * speed;
half frequency = (direction.x * positionWS.x + direction.y * positionWS.z) * length - phase;
float3 offset = 0;
offset.y = amplitude * sin(frequency);
offset.xz = qi * amplitude * direction.xy * cos(frequency);
return offset;
}根据 Gerstner wave 顶点偏移的平面
水平偏移
为顶点添加 正弦波,那样子能有更加丰富的顶点变化,边会有一定弧度,不会直直的,这里只改变 x 和 z 轴~
float3 CalculateHorizontalOffset(float3 positionWS, half lenght, half speed, half2 direction)
{
float time = _Time.y;
float phase = time * speed;
float frequency = sin(((direction.x * positionWS.x + direction.y * positionWS.z) * lenght) - phase);
float3 offset = 0;
offset.x = direction.x * frequency;
offset.z = direction.y * frequency;
return offset;
}根据 正弦函数 顶点偏移的平面
把这两个顶点计算整合到一个方法里:
float3 TransformObjectToWaveWorld(float4 positionOS)
{
float3 positionWS = TransformObjectToWorld(positionOS.xyz);
float3 offset = 0;
positionWS += CalculateHorizontalOffset(positionWS, _HorizontalOffset0.x, _HorizontalOffset0.y, _HorizontalOffset0.zw);
offset += CalculateGerstnerWaveOffset(positionWS, _WaveGerstner0.x, _WaveGerstner0.y, _WaveGerstner0.z, _WaveGerstner0.w, 4);
offset += CalculateGerstnerWaveOffset(positionWS, _WaveGerstner1.x, _WaveGerstner1.y, _WaveGerstner1.z, _WaveGerstner1.w, 4);
offset += CalculateGerstnerWaveOffset(positionWS, _WaveGerstner2.x, _WaveGerstner2.y, _WaveGerstner2.z, _WaveGerstner2.w, 4);
offset += CalculateGerstnerWaveOffset(positionWS, _WaveGerstner3.x, _WaveGerstner3.y, _WaveGerstner3.z, _WaveGerstner3.w, 4);
return positionWS + offset;
}
2.2 - 法线计算
计算法线 需要知道 三角形面 的三个点,方法目前我知道的方法有两个,写在下面~
法线计算公式是 :
float3 CalculateNormal(float3 pos0, float3 pos1, float3 pos2)
{
float3 normal = cross(pos1 - pos0, pos2 - pos0);
return normalize(normal);
}不重新计算法线的后果
缓存坐标到UV计算法线
根据 Mesh 把三角面的坐标分别缓存到 UV0 和 UV1 内,在 Vertex Shader 里面计算其它顶点坐标得到法线。代码实现是这样:
struct Attributes
{
float4 positionOS : POSITION;
float3 texcoord0 : TEXCOORD0;
float3 texcoord1 : TEXCOORD1;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 normalWS : TEXCOORD1;
}
Varyings Vertex(Attributes input)
{
Varyings output = (Varyings)0;
var pos0 = TransformObjectToWaveWorld(input.positionOS);
var pos1 = TransformObjectToWaveWorld(input.texcoord0);
var pos2 = TransformObjectToWaveWorld(input.texcoord1);
output.positionCS = TransformWorldToHClip(pos0);
output.normalWS = CalculateNormal(pos0, pos1, pos2);
return output;
}这方法比较通用,缺点也是显而易见的,就是 :
每个顶点需要多计算2次波浪;输入模型需要预先处理,缓存坐标数据到UV;不支持 Tessellation(曲面细分);
使用 Geometry Shader(几何着色器) 计算法线
这是在 DX10、OpenGL4.1 添加的 顶点处理阶段 功能,可以将单个基本体作为输入,输出零个或多个基本体,意味着我们可以使用这功能获取到 三角形图元顶点信息,很方便的计算法线,代码如下:
struct Attributes
{
float4 positionOS : POSITION;
};
struct Varyings
{
float3 positionWS : TEXCOORD1;
float3 normalWS : TEXCOORD2;
float4 positionCS : SV_POSITION;
};
Varyings Vertex(Attributes input)
{
Varyings output = (Varyings)0;
output.positionWS = TransformObjectToWaveWorld(input.positionOS);
output.normalWS = 0;
output.positionCS = TransformWorldToHClip(output.positionWS);
return output;
}
[maxvertexcount(3)]
void Geometry(triangle Varyings input[3], inout TriangleStream<Varyings> outputStream)
{
Varyings input0 = input[0];
Varyings input1 = input[1];
Varyings input2 = input[2];
float3 normalWS = CalculateNormal(input0.positionWS, input1.positionWS, input2.positionWS);
input0.normalWS = input1.normalWS = input2.normalWS = normalWS;
outputStream.Append(input0);
outputStream.Append(input1);
outputStream.Append(input2);
}法线计算在 Geometry 函数里进行,对比上个方法性能友好许多,但这个方法也有缺点,就是不支持 Metal,也就是 苹果设备,具体信息可以看这里 :Unity Manual : Metal
法线计算总结
从 UV 计算 | 在 Geometry Shader 计算 | 顶点阶段性能 | 多2次波浪计算 | 一次搞定 | DirectX | 不限制 | 最低 DX10 | OpenGL | 不限制 | 最低 OpenGL 4.1 | OpenGLES | 不限制 | 最低 OpenGLES 3.2 | Metal | 不限制 | 不支持 | Tessellation | 不支持 | 支持 | 从UV计算法线: 可以发布到苹果,移动端需要尽量减少网格顶点数量优化性能~
在 Geometry Shader 计算:不支持苹果,但是支持安卓!!!支持 Tessellation,如果地图内需要实现很广的一片水域,可以用 Tessellation 优化性能,越远细分值越低。
安卓掰回一局
3.0 - 着色器片段阶段
3.1 - 水底折射
3.2 - 阴影接收和投射
3.3 - 使用 BlinnPhong 光照模型
3.4 - 边缘泡沫
3.5 - 背面光照
3.6 - 点光源
3.7 - 水面反射
4.0 - 其它功能
4.1 - Tessellation(曲面细分)
5.0 - 代码汇总
完整的源码在这,动态更新 :
LPWater_VertexTest.shader
Shader &#34;JiongXiaXia/LPWater/VertexTest&#34;
{
Properties
{
//Horizontal Offset
[Header(length speed direction)]
_HorizontalOffset0 (&#34;VertexHorizontalOffset0&#34;, Vector) = (2, 0.5, 0.3, 0.3)
//Gerstner Wave
[Header(amplitude length speed angle)]
_WaveGerstner0 (&#34;Gerstner wave 0&#34;, Vector) = (0.0001, 1, 1, 0)
_WaveGerstner1 (&#34;Gerstner wave 1&#34;, Vector) = (0.0001, 1, 1, 0)
_WaveGerstner2 (&#34;Gerstner wave 2&#34;, Vector) = (0.0001, 1, 1, 0)
_WaveGerstner3 (&#34;Gerstner wave 3&#34;, Vector) = (0.0001, 1, 1, 0)
//Lighting
_Albedo (&#34;Albedo&#34;, COLOR) = (0.76, 0.94, 0.93, 1)
_Metallic (&#34;Metallic&#34;, Range(0, 1)) = 0
_Smoothness (&#34;Smoothness&#34;, Range(0, 1)) = 0
[Enum(UnityEngine.Rendering.CullMode)] _Cull (&#34;Cull&#34;, float) = 2
}
HLSLINCLUDE
#include &#34;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl&#34;
CBUFFER_START(UnityPerMaterial)
half4 _HorizontalOffset0;
half4 _WaveGerstner0;
half4 _WaveGerstner1;
half4 _WaveGerstner2;
half4 _WaveGerstner3;
half4 _Albedo;
float _Metallic;
float _Smoothness;
CBUFFER_END
float3 CalculateNormal(float3 pos0, float3 pos1, float3 pos2)
{
float3 normal = cross(pos1 - pos0, pos2 - pos0);
return normalize(normal);
}
float3 CalculateHorizontalOffset(float3 positionWS, half lenght, half speed, half2 direction)
{
float time = _Time.y;
float phase = time * speed;
float frequency = sin(((direction.x * positionWS.x + direction.y * positionWS.z) * lenght) - phase);
float3 offset = 0;
offset.x = direction.x * frequency;
offset.z = direction.y * frequency;
return offset;
}
float3 CalculateGerstnerWaveOffset(float3 positionWS, half amplitude, half length, half speed, half angle, int partCount)
{
half radian = angle * PI / 180.0;
half2 direction = half2(sin(radian), cos(radian));
half w = sqrt(2.0 * PI * 9.81f / length);
half qi = 1.0 / (amplitude * w * partCount);
half time = _Time.y;
half phase = time * speed;
half frequency = (direction.x * positionWS.x + direction.y * positionWS.z) * length - phase;
float3 offset = 0;
offset.y = amplitude * sin(frequency);
offset.xz = qi * amplitude * direction.xy * cos(frequency);
return offset;
}
float3 TransformObjectToWaveWorld(float4 positionOS)
{
float3 positionWS = TransformObjectToWorld(positionOS.xyz);
float3 offset = 0;
positionWS += CalculateHorizontalOffset(positionWS, _HorizontalOffset0.x, _HorizontalOffset0.y, _HorizontalOffset0.zw);
offset += CalculateGerstnerWaveOffset(positionWS, _WaveGerstner0.x, _WaveGerstner0.y, _WaveGerstner0.z, _WaveGerstner0.w, 4);
offset += CalculateGerstnerWaveOffset(positionWS, _WaveGerstner1.x, _WaveGerstner1.y, _WaveGerstner1.z, _WaveGerstner1.w, 4);
offset += CalculateGerstnerWaveOffset(positionWS, _WaveGerstner2.x, _WaveGerstner2.y, _WaveGerstner2.z, _WaveGerstner2.w, 4);
offset += CalculateGerstnerWaveOffset(positionWS, _WaveGerstner3.x, _WaveGerstner3.y, _WaveGerstner3.z, _WaveGerstner3.w, 4);
return positionWS + offset;
}
ENDHLSL
SubShader
{
Tags { &#34;RenderType&#34;=&#34;Transparent&#34; &#34;RenderPipeline&#34;=&#34;UniversalPipeline&#34; &#34;IgnoreProjector&#34;=&#34;True&#34; &#34;Queue&#34;=&#34;Transparent-1&#34; }
Pass
{
Name &#34;ForwardLit&#34;
Tags { &#34;LightMode&#34;=&#34;UniversalForward&#34; }
ZWrite On
Cull [_Cull]
HLSLPROGRAM
#pragma vertex LitPassVertex
#pragma geometry LitPassGeometry
#pragma fragment LitPassFragment
struct Attributes
{
float4 positionOS : POSITION;
};
struct Varyings
{
float3 positionWS : TEXCOORD1;
float3 normalWS : TEXCOORD2;
float3 viewDirectionWS : TEXCOORD3;
float4 positionCS : SV_POSITION;
};
Varyings LitPassVertex(Attributes input)
{
Varyings output = (Varyings)0;
output.positionWS = TransformObjectToWaveWorld(input.positionOS);
output.normalWS = 0;
output.positionCS = TransformWorldToHClip(output.positionWS);
output.viewDirectionWS = normalize(GetCameraPositionWS() - output.positionWS);
return output;
}
[maxvertexcount(3)]
void LitPassGeometry(triangle Varyings input[3], inout TriangleStream<Varyings> outputStream)
{
Varyings input0 = input[0];
Varyings input1 = input[1];
Varyings input2 = input[2];
float3 worldNormal = CalculateNormal(input0.positionWS, input1.positionWS, input2.positionWS);
input0.normalWS = input1.normalWS = input2.normalWS = worldNormal;
outputStream.Append(input0);
outputStream.Append(input1);
outputStream.Append(input2);
}
half4 LitPassFragment(Varyings input) : SV_Target
{
InputData inputData = (InputData)0;
inputData.positionWS = input.positionWS;
inputData.normalWS = input.normalWS;
inputData.viewDirectionWS = input.viewDirectionWS;
half4 color = UniversalFragmentPBR(inputData, _Albedo.rgb, _Metallic, 1, _Smoothness, 1, 0, 1);
return color;
}
ENDHLSL
}
}
FallBack &#34;Hidden/InternalErrorShader&#34;
} |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|