Lowpoly风格水体着色器-Unity Shader教学(施工中)

[复制链接]
慧眼识英雄1 发表于 2022-4-20 21:13:06 | 显示全部楼层 |阅读模式
1.0 - 概论

Lowpoly风格水体 我之前做过两次,一个是基于 Unity built-in 渲染管道的,现已经开源在Github 点这里 。
还有一个是基于 Unity LWRP 渲染管道的,这个源代码就不放上来了, 因为功能叠的太多而且写的很烂,API 过时也懒得改了...
如今这里这个是基于 Unity URP 渲染管道,做为 Lowpoly游戏制做指南 的一部门,这是份实用向的指南,希望能协助到大家吧~噢耶! ヾ( ̄▽ ̄)~
(文章还在施工中,我会尽快补完)
程度有限多多包容,先看看最终效果吧~!

Lowpoly风格水体着色器-Unity Shader教学(施工中)-1.jpg

BlinnPhong 光照 + 简单浮力 + 水面反射

Lowpoly风格水体着色器-Unity Shader教学(施工中)-2.jpg

水底折射 + 焦散 + 通明物体撑持

Lowpoly风格水体着色器-Unity Shader教学(施工中)-3.jpg

船体内部裁剪 + 边沿泡沫

Lowpoly风格水体着色器-Unity Shader教学(施工中)-4.jpg

无缝的水底切换 + 水底雾

Lowpoly风格水体着色器-Unity Shader教学(施工中)-5.jpg

后背光照 + 水底雾

Lowpoly风格水体着色器-Unity Shader教学(施工中)-6.jpg

海域

实现以上功能需要理解 :

  • Unity
  • Unity Shader
  • Unity URP
  • C#
  • 一点点 三维模型常识 和 图形学常识
原理也不难,我能写代码就尽量写代码吧~ ╰( ̄▽ ̄)╮

Lowpoly风格水体着色器-Unity Shader教学(施工中)-7.jpg

Talk is cheap. Show me the code.

1.1- 阐述和实现

大家都知道 Lowpoly风格 模型通常显得块状且简单,由多个三角形组合,二维暗示是这样 ↓

Lowpoly风格水体着色器-Unity Shader教学(施工中)-8.jpg

Lowpoly 风格 二维

我们要实现的水体略微高级一些,是用 Shader(着色器)制做。水面是有海浪的,随着 相机 或 光源 角度位置的变革,水面呈现出不同颜色,像下面这张动图这样~

Lowpoly风格水体着色器-Unity Shader教学(施工中)-9.gif

日出日落

像海浪一般用的是 Gerstner wave算法,这算法能够计算出水面的 坐标法线,但因为是 Lowpoly 风格水面高度由顶点决定,所以 Gerstner wave的法线计算公式就不适用了,我们需要在运行时为每个三角面从头计算法线,以到达下面图片的效果~

Lowpoly风格水体着色器-Unity Shader教学(施工中)-10.jpg

Lowpoly风格水体

Lowpoly风格水体着色器-Unity Shader教学(施工中)-11.jpg

一般水体(来自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风格水体着色器-Unity Shader教学(施工中)-12.jpg

简单光照的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;
}
Lowpoly风格水体着色器-Unity Shader教学(施工中)-13.jpg

根据 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;
}
Lowpoly风格水体着色器-Unity Shader教学(施工中)-14.jpg

根据 正弦函数 顶点偏移的平面

把这两个顶点计算整合到一个方案里:
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;
}
Lowpoly风格水体着色器-Unity Shader教学(施工中)-15.jpg

2.2 - 法线计算
计算法线 需要知道 三角形面 的三个点,方案目前我知道的方案有两个,写在下面~
法线计算公式是 :
float3 CalculateNormal(float3 pos0, float3 pos1, float3 pos2)
{
    float3 normal = cross(pos1 - pos0, pos2 - pos0);
    return normalize(normal);
}
Lowpoly风格水体着色器-Unity Shader教学(施工中)-16.jpg

不从头计算法线的后果

缓存坐标到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 优化性能,越远细分值越低。

Lowpoly风格水体着色器-Unity Shader教学(施工中)-17.jpg

安卓掰回一局

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 "JiongXiaXia/LPWater/VertexTest"
{
    Properties
    {
        //Horizontal Offset
        [Header(length speed direction)]
        _HorizontalOffset0 ("VertexHorizontalOffset0", Vector) = (2, 0.5, 0.3, 0.3)

        //Gerstner Wave
        [Header(amplitude length speed angle)]
        _WaveGerstner0 ("Gerstner wave 0", Vector) = (0.0001, 1, 1, 0)
        _WaveGerstner1 ("Gerstner wave 1", Vector) = (0.0001, 1, 1, 0)
        _WaveGerstner2 ("Gerstner wave 2", Vector) = (0.0001, 1, 1, 0)
        _WaveGerstner3 ("Gerstner wave 3", Vector) = (0.0001, 1, 1, 0)

        //Lighting
        _Albedo ("Albedo", COLOR) = (0.76, 0.94, 0.93, 1)
        _Metallic ("Metallic", Range(0, 1)) = 0
        _Smoothness ("Smoothness", Range(0, 1)) = 0

        [Enum(UnityEngine.Rendering.CullMode)] _Cull ("Cull", float) = 2
    }

    HLSLINCLUDE

    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

    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 { "RenderType"="Transparent" "RenderPipeline"="UniversalPipeline" "IgnoreProjector"="True" "Queue"="Transparent-1" }

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForward" }       

            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 "Hidden/InternalErrorShader"
}
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

菜鸟C4D推荐上一条 /9 下一条

菜鸟C4D与你一起从零开始!
菜鸟C4D微信公众号