Unity 低功耗玉石

发布时间 2023-11-22 18:23:42作者: 爱莉希雅

前言

曾使用过UE5的substrate系统基于BSDF实现过玉石材质,效果雀氏nice但消耗太高了!因此本篇基于Unity介绍如何模拟透射来实现一个低功耗的玉石材质
效果如下

本篇同步发布于http://chenglixue.top/index.php/unity/90/

总体框架

模拟透射光

  • 思路

    • 因为透射现象是一种光打在物体发生散射,其中一部分光进入物体内部,且这一部分的光的某一部分成功穿过物体背面,最终我们便可以看到下图现象,所以很明显为了模拟这一现象,需要考虑相机方向向量 和 光源方向向量

    • 但仅仅考虑相机方向向量和光源方向向量还不够,因为光线发生散射时,进入物体内部的光线方向是随机的,所以需要想办法对原光线进行扰动

    • 最后还需考虑厚度对透射的影响

实现

模拟透射

  • 可以通过扰动法线方向来模拟散射光线

    // 模拟散射光线
    float3 scatterMainLightDirw = -normalize(mainLightDirW + normalW * _DistortMainLightDir);
    
  • 随后需要考虑相机方向向量 和 光源方向向量

    // 对透射物体的光线向量和视角向量进行dot
    float VoInvL = saturate(dot(viewDirW, scatterMainLightDirw));
    
    // pow控制透射对比度
    // _MainLightScatterIntensity控制透射强度
    VoInvL = pow(VoInvL, _Contrast) * _MainLightScatterIntensity;
    

    下图展示了透射的对比度

  • 计算透射颜色

    // 模型的厚度贴图.值越高,越厚,越不容易透射
    float thickness = SAMPLE_TEXTURE2D(_ThicknessMap, sampler_ThicknessMap, psInput.uv).r;
    
    // _ScatterColor.rgb控制透射颜色
    half3 scatterColor = mainLightColor * VoInvL * (1 - thickness) * _ScatterColor.rgb;
    

漫反射

  • 模拟的透射光并没有乘上diffuse纹理,若不添加diffuse计算会导致背对光源看向模型,模型是纯黑的。并添加一个_AddColor来手动调整玉石颜色

    half4 diffuseTex = SAMPLE_TEXTURE2D(_DiffuseMap, sampler_DiffuseMap, psInput.uv) * _DiffuseTint;
    
    // half lambert
    float NoL = saturate(dot(normalW, mainLightDirW) * 0.5 + 0.5);
    
    half3 diffuseColor = diffuseTex * mainLightColor * NoL;
    
    diffuseColor += _AddColor.rgb;  // 手动添加的额外颜色
    

环境光反射

  • 因为是反射,所以可以添加fresnel效果进行丰富

    TEXTURECUBE(_CubeMap);                  SAMPLER(sampler_CubeMap);       float4 _CubeMap_HDR;
    
    float Fresnel(float VoN, float Power)
    {
        return pow(1.0 - saturate(VoN), Power);
    }
    
    float3 fresnel = Fresnel(VoN, _FresnelExp);
    // 环境光反射
    half4 cubemapColor = SAMPLE_TEXTURECUBE(_CubeMap, sampler_CubeMap, reflectDirW);
    // 解码HDR
    half3 environmentColor = DecodeHDREnvironment(cubemapColor, _CubeMap_HDR) * fresnel * _Exposure;
    
    

模拟天光

  • 这里模拟的天光也就是一个类似AO的效果

    half3 skyLightColor = saturate(dot(normalW, float3(0, 1, 0) * 0.5 + 0.5)) * diffuseTex * _SkyLightOpacity;
    

多光源

  • 这里只考虑额外光源的透射效果即可(这样美术效果更为合适)

    [KeywordEnum(OFF, ON)]_ADDITION_LIGHT("Addition Light ?", Int) = 1
    
    #pragma shader_feature_local _ADDITION_LIGHT_ON _ADDITION_LIGHT_OFF
    
    #if defined _ADDITION_LIGHT_ON
    
    // 额外光源的数量
    int addLightCounts = GetAdditionalLightsCount();	
    for(int i = 0; i < addLightCounts; ++i)
    {
        Light addLight = GetAdditionalLight(i, psInput.positionW);
        float3 addLightDirW = normalize(addLight.direction);	// 额外光源的方向
        half3 addLightColor = addLight.color;	// 额外光源的颜色
    
        // 模拟散射光
        float3 scatterAddLightDirw = -normalize(addLightDirW + normalW * _DistortAddLightDir);
        float addLightVoInvL = saturate(dot(viewDirW, scatterAddLightDirw));
        addLightVoInvL = pow(addLightVoInvL, _Contrast) * _AddLightScatterIntensity;
        half3 addLightScatterColor = addLightColor * (1 - thickness) * addLightVoInvL * addLight.distanceAttenuation * addLight.shadowAttenuation;
    
        outputColor += addLightScatterColor;
    }
    #endif
    

全部代码

  • Shader

    {
        Properties
        {
            [Header(Diffuse Setting)]
            [Space(3)]
            [MainTexture]_DiffuseMap("Diffuse Texture", 2D) = "white" {}
            [HDR][MainColor]_DiffuseTint("Diffuse Color Tint", Color) = (1, 1, 1, 1)
            [HDR]_AddColor("Additional Color", Color) = (1, 1, 1, 1)
            [Space(30)]
            
            [Header(Light Setting)]
            _DistortMainLightDir("Distort Main Light Direction", Range(0, 1)) = 0
            _DistortAddLightDir("Distort Add Light Direction", Range(0, 1)) = 0
            _Contrast("Contrast", Float) = 5
            _MainLightScatterIntensity("Main Light Scatter Intensity", Float) = 1
            _AddLightScatterIntensity("Addition Light Scatter Intensity", Float) = 1
            [HDR]_ScatterColor("Scatter Color", Color) = (1, 1, 1, 1)
            _SkyLightOpacity("SkyLight Opacity", Range(0, 1)) = 0
            [KeywordEnum(OFF, ON)]_ADDITION_LIGHT("Addition Light ?", Int) = 1
            [Space(30)]
            
            [Header(Thickness Setting)]
            _ThicknessMap("Thickness Tex", 2D) = "white" {}
            [Space(30)]
            
            [Header(Environment Setting)]
            _CubeMap("Cube map", Cube) = "white" {}
            _Exposure("Exposure", Range(0, 8)) = 1
            _Rotation("Rotation",Range(0,360)) = 0  
            _FresnelExp("Fresnel Exp", Range(1, 10)) = 5
        }
        
        SubShader
        {
            Tags
            {
                "Pipeline" = "UniversalPipeline"
                "RenderType" = "Opaque"
                "Queue" = "Geometry"
            }
            LOD 100
            
            HLSLINCLUDE
            
            ENDHLSL
    
            Pass
            {
                Tags
                {
                    "LightMode" = "UniversalForward"
                }
                
                HLSLPROGRAM
    
                #pragma shader_feature_local _ADDITION_LIGHT_ON _ADDITION_LIGHT_OFF
                #pragma multi_compile __ _MAIN_LIGHT_SHADOWS    // 计算阴影衰减
                #pragma multi_compile __ _MAIN_LIGHT_SHADOWS_CASCADE    // 得到正确的阴影坐标
                #pragma multi_compile __ _SHADOWS_SOFT  //计算软阴影
                #pragma multi_compile __ ADDITIONAL_LIGHT_CALCULATE_SHADOWS // 计算额外光的阴影衰减和距离衰减
                #pragma multi_compile __ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS   //计算阴影投射
                
                #include_with_pragmas "Assets/Jade/Library/Jade.hlsl"
                
                #pragma vertex VS
                #pragma fragment PS
                ENDHLSL
            }
    
            Pass
            {
                Tags
                {
                    "LightMode" = "ShadowCaster"
                }
                
                HLSLPROGRAM
    
                #include_with_pragmas "Assets/Jade/Library/Jade.hlsl"
                
                #pragma vertex VSShadow
                #pragma fragment PSShadow
                
                ENDHLSL
                }   
            }
    }
    
  • HLSL

    #pragma once
    
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
    // 个人的工具库
    #include "Assets/Shader/MyUtil/MyUtil.hlsl"	
    
    // -------------------------------------------- variable definition --------------------------------------------
    CBUFFER_START(UnityPerMaterial)
    
    float4 _DiffuseMap_ST;
    half4 _DiffuseTint;
    half4 _AddColor;
    
    float _DistortMainLightDir;
    float _DistortAddLightDir;
    float _Contrast;
    float _MainLightScatterIntensity;
    float _AddLightScatterIntensity;
    half4 _ScatterColor;
    
    float _Exposure;
    float _Rotation;
    float _FresnelExp;
    
    float _SkyLightOpacity;
    
    // 获取主光源和其余光源的方向(Unity自动完成赋值)
    half3 _LightDirection;
    
    CBUFFER_END
    
    TEXTURE2D(_DiffuseMap);                 SAMPLER(sampler_DiffuseMap);
    TEXTURE2D(_ThicknessMap);               SAMPLER(sampler_ThicknessMap);
    TEXTURECUBE(_CubeMap);                  SAMPLER(sampler_CubeMap);       float4 _CubeMap_HDR;
    
    struct VSInput
    {
        float4 positionL : POSITION;
        float3 normalL : NORMAL;
        float2 uv : TEXCOORD0;
    };
    
    struct PSInput
    {
        float4 positionH : SV_POSITION;
        float4 shadowUV : TEXCOORD3;
        
        float3 normalW : TEXCOORD0;
        float3 positionW : TEXCOORD1;
        
        float2 uv : TEXCOORD2;
    };
    
    // -------------------------------------------- function definition --------------------------------------------
    
    PSInput VS(VSInput vsInput)
    {
        PSInput vsOutput;
    
        // Get pos
        VertexPositionInputs vertexPosInput = GetVertexPositionInputs(vsInput.positionL);
        vsOutput.positionH = vertexPosInput.positionCS;
        vsOutput.positionW = vertexPosInput.positionWS;
    
        // Get normal
        VertexNormalInputs vertexNormalInput = GetVertexNormalInputs(vsInput.normalL);
        vsOutput.normalW = vertexNormalInput.normalWS;
    
        // Get uv
        vsOutput.uv = TRANSFORM_TEX(vsInput.uv, _DiffuseMap);
        vsOutput.shadowUV = TransformWorldToShadowCoord(vsOutput.positionW);
    
        return vsOutput;
    }
    
    half4 PS(PSInput psInput) : SV_TARGET
    {
        half3 outputColor = 0.f;
    
        // about texture
        half4 diffuseTex = SAMPLE_TEXTURE2D(_DiffuseMap, sampler_DiffuseMap, psInput.uv) * _DiffuseTint;
        float thickness = SAMPLE_TEXTURE2D(_ThicknessMap, sampler_ThicknessMap, psInput.uv).r;
            
        // about light
        Light mainLight = GetMainLight(psInput.shadowUV);
        float3 mainLightDirW = normalize(mainLight.direction);
        half3 mainLightColor = mainLight.color;
    
        // about direction
        float3 normalW = normalize(psInput.normalW);
        float3 viewDirW = normalize(GetCameraPositionWS() - psInput.positionW);
        // 模拟散射光线
        float3 scatterMainLightDirw = -normalize(mainLightDirW + normalW * _DistortMainLightDir);
        // 环境光反射
        float3 reflectDirW = normalize(reflect(-viewDirW, normalW));
        reflectDirW = RotatEnvironment(_Rotation, reflectDirW);
    
        float NoL = saturate(dot(normalW, mainLightDirW) * 0.5 + 0.5);
        float VoN = saturate(dot(viewDirW, normalW));
        float VoInvL = saturate(dot(viewDirW, scatterMainLightDirw));   // 模拟透射现象
        VoInvL = pow(VoInvL, _Contrast) * _MainLightScatterIntensity;
        float3 fresnel = Fresnel(VoN, _FresnelExp);
    
        // result color
        // diffuse
        half3 diffuseColor = diffuseTex * mainLightColor * NoL;
        diffuseColor += _AddColor.rgb;  // 手动添加的额外颜色
        // 模拟透射光
        half3 scatterColor = mainLightColor * VoInvL * (1 - thickness) * _ScatterColor.rgb;
        // 环境光反射
        half4 cubemapColor = SAMPLE_TEXTURECUBE(_CubeMap, sampler_CubeMap, reflectDirW);
        half3 environmentColor = DecodeHDREnvironment(cubemapColor, _CubeMap_HDR) * fresnel * _Exposure;
        // 模拟天光
        half3 skyLightColor = saturate(dot(normalW, float3(0, 1, 0) * 0.5 + 0.5)) * diffuseTex * _SkyLightOpacity;
    
        #if defined _ADDITION_LIGHT_ON
    
        int addLightCounts = GetAdditionalLightsCount();
        for(int i = 0; i < addLightCounts; ++i)
        {
            Light addLight = GetAdditionalLight(i, psInput.positionW, half4(1.f, 1.f, 1.f, 1.f));
            float3 addLightDirW = normalize(addLight.direction);
            half3 addLightColor = addLight.color;
    
            // 模拟散射光
            float3 scatterAddLightDirw = -normalize(addLightDirW + normalW * _DistortAddLightDir);
            float addLightVoInvL = saturate(dot(viewDirW, scatterAddLightDirw));
            addLightVoInvL = pow(addLightVoInvL, _Contrast) * _AddLightScatterIntensity;
            half3 addLightScatterColor = addLightColor * (1 - thickness) * addLightVoInvL * addLight.distanceAttenuation * addLight.shadowAttenuation;
    
            outputColor += addLightScatterColor;
        }
        #endif
    
        outputColor += diffuseColor + scatterColor + environmentColor + skyLightColor;
        
        return half4(outputColor, 1.f);
    }
    
    PSInput VSShadow(VSInput vsInput)
    {
        PSInput VSOutput;
    
        VSOutput.uv = TRANSFORM_TEX(vsInput.uv, _DiffuseMap);
    
        VSOutput.normalW = TransformObjectToWorldNormal(vsInput.normalL);
        VSOutput.positionW = TransformObjectToWorld(vsInput.positionL);
        Light mainLight = GetMainLight();
                    
        VSOutput.positionH = TransformWorldToHClip(ApplyShadowBias(VSOutput.positionW, VSOutput.normalW, _LightDirection));
                    
        #if UNITY_REVERSED_Z
        VSOutput.positionH.z = min(VSOutput.positionH.z, VSOutput.positionH.w * UNITY_NEAR_CLIP_VALUE);
        #else
        VSOutput.positionH.z = max(VSOutput.positionH.z, VSOutput.positionH.w * UNITY_NEAR_CLIP_VALUE);
        #endif
    
        return VSOutput;
    }
    
    half4 PSShadow(PSInput psInput) : SV_TARGET
    {
        return 0;
    }
    

reference

星云大神的RTR3