找回密码
 立即注册
查看: 453|回复: 0

[简易教程] Lowpoly风格水体着色器-Unity Shader教程(施工中)

[复制链接]
发表于 2021-5-12 09:31 | 显示全部楼层 |阅读模式
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 "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"
}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-20 07:31 , Processed in 0.091669 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表