1.0 - 概论
Lowpoly风格水体 我之前做过两次,一个是基于 Unity built-in 渲染管道的,现已经开源在Github 点这里 。
还有一个是基于 Unity LWRP 渲染管道的,这个源代码就不放上来了, 因为功能叠的太多而且写的很烂,API 过时也懒得改了...
如今这里这个是基于 Unity URP 渲染管道,做为 Lowpoly游戏制做指南 的一部门,这是份实用向的指南,希望能协助到大家吧~噢耶! ヾ( ̄▽ ̄)~
(文章还在施工中,我会尽快补完)
程度有限多多包容,先看看最终效果吧~!
BlinnPhong 光照 + 简单浮力 + 水面反射
水底折射 + 焦散 + 通明物体撑持
船体内部裁剪 + 边沿泡沫
无缝的水底切换 + 水底雾
后背光照 + 水底雾
海域
实现以上功能需要理解 :
- Unity
- Unity Shader
- Unity URP
- C#
- 一点点 三维模型常识 和 图形学常识
原理也不难,我能写代码就尽量写代码吧~ ╰( ̄▽ ̄)╮
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;
} |
|