跳到主要内容
← 全部 Shader
浏览器实时运行 HLSL 移植 原始语言:HLSL (Unity URP) 浏览器:GLSL ES 3.00

URP RimLit 移植示例(HLSL → GLSL ES 3.00)

一个按 Unity URP 惯例编写的 RimLitUnlit HLSL 着色器,以及它的浏览器 GLSL ES 3.00 移植版本。页面给出完整的参数映射表、无法一致的部分及其原因,并如实标注验证方式。

GLSL ES 3.00 三维网格 · sphere 等待初始化
URP RimLit 移植版本的静态截图:球体上带有青色边缘光

静态预览:实时渲染需要启用 JavaScript 与支持 WebGL 2 的浏览器。初始化成功后会自动切换为实时画面。

— 0.0 s
预设
#2A6BC0

对应 HLSL 的 _BaseColor。Unity 在线性空间项目中会把调色板的 sRGB 值转换成线性后交给着色器,移植版本做了同样的转换。 uniform uBaseColor

#59F2FF

对应 [HDR] _RimColor。浏览器预览不支持 HDR 颜色,超过 1 的分量会被裁剪。 uniform uRimColor

#FFFFFF

对应 [HDR] _SpecularColor。 uniform uSpecularColor

#0F1218

对应 _AmbientColor。URP 中真实的环境光来自光照探针与 IBL,这里保留为一个常量颜色以便逐像素对照。 uniform uAmbientColor

#FFF2DC

对应 GetMainLight().color,也就是 URP 每帧写入的 _MainLightColor。 uniform uLightColor

(0.40, 0.85, 0.35)

对应 GetMainLight().direction(_MainLightPosition)。URP 由引擎每帧填充,浏览器里只能作为显式 uniform 暴露。 uniform uLightDirection

1.00

对应 mainLight.shadowAttenuation。浏览器预览没有阴影贴图,因此它被降级为一个可手动调节的常量。 uniform uShadowAttenuation

3.00

对应 _RimPower,Fresnel 项的指数。 uniform uRimPower

1.20

对应 _RimIntensity。 uniform uRimIntensity

0.25

对应 _RimPulse,控制边缘光随时间脉动的幅度,0 表示关闭。 uniform uRimPulse

1.50

对应 _PulseSpeed,与 _Time.y 相乘得到脉动相位。 uniform uPulseSpeed

0.60

对应 _Smoothness,通过 exp2(lerp(3, 11, s)) 映射为 Blinn-Phong 指数。 uniform uSmoothness

运行环境与资源诊断
尚未初始化。

HLSL → GLSL 移植记录

未在 Unity 中对照验证 本仓库没有安装 Unity,因此 HLSL 只做了静态审查(属性、CBUFFER、URP 头文件、 光照函数用法),GLSL 版本则在无头 Chromium + SwiftShader 中实际编译、链接并渲染, 由 tests/shader-live.spec.ts 断言出图与参数生效。两者的逐行对照是人工完成的, 没有在 Unity 编辑器中做过并排截图比对。

参数映射表

HLSL 属性到 GLSL uniform 的映射
Unity URP (HLSL) 浏览器 (GLSL ES 3.00) 说明
_BaseColor / _RimColor / _SpecularColor / _AmbientColor uBaseColor / uRimColor / uSpecularColor / uAmbientColor half4 -> vec3(alpha 在浏览器版本中被固定为 1)。线性空间项目下 Unity 会把 sRGB 调色板值转成线性再上传;移植版本用 THREE.Color.setStyle(hex, SRGBColorSpace) 做同样的事。
_RimPower / _RimIntensity / _RimPulse / _PulseSpeed / _Smoothness uRimPower / uRimIntensity / uRimPulse / uPulseSpeed / uSmoothness half -> float,名称与取值范围完全一致。
GetMainLight().direction uLightDirection URP 由引擎每帧写入 _MainLightPosition;浏览器没有这份数据,只能作为显式 uniform 暴露给访问者。
GetMainLight().color uLightColor 对应 _MainLightColor,同样是引擎填充的。
mainLight.shadowAttenuation uShadowAttenuation 浏览器预览没有阴影贴图,也没有 _MAIN_LIGHT_SHADOWS 关键字;降级为可手动调节的常量,默认 1。
_Time.y uTime Unity 的 _Time.y 是场景加载以来的秒数且不可暂停;预览使用自己的可暂停时钟,暂停时 uTime 停止推进。
_WorldSpaceCameraPos cameraPosition three.js 自动上传同名 uniform,但 RawShaderMaterial 不会自动声明,必须在着色器里手写。
TransformObjectToWorld(positionOS) (modelMatrix * vec4(position, 1.0)).xyz 等价。
TransformWorldToHClip(positionWS) projectionMatrix * modelViewMatrix * vec4(position, 1.0) three.js 的 modelViewMatrix 已经包含视图矩阵,结果一致。
TransformObjectToWorldNormal(normalOS) normalize(mat3(modelMatrix) * normal) URP 用的是模型矩阵逆转置。本页网格缩放恒为 1,两者等价;若引入非均匀缩放需要改成 inverse(transpose(mat3(modelMatrix)))。
RimLitShininess(s) = exp2(lerp(3, 11, saturate(s))) rimLitShininess(s) = exp2(mix(3.0, 11.0, clamp(s, 0.0, 1.0))) 逐字对应,saturate -> clamp(x, 0.0, 1.0)。
RimLitFresnel(N, V, p) = pow(1 - saturate(dot(N,V)), max(p, 0.0001)) rimLitFresnel(N, V, p) 逐字对应。
return half4(color, _BaseColor.a) // 写入 sRGB framebuffer fragColor = vec4(linearToSRGB(color), 1.0) 最重要的差异。Unity 线性工作流由 sRGB framebuffer 在硬件里完成转换;three.js 不会为 RawShaderMaterial 追加任何输出转换,因此必须在片元着色器里显式做 linear -> sRGB。

无法完全一致的部分

  • 没有阴影。URP 的 TransformWorldToShadowCoord 与 _MAIN_LIGHT_SHADOWS / _SHADOWS_SOFT 关键字在浏览器预览中没有对应物,shadowAttenuation 只能是常量。
  • 没有环境光照探针与 IBL。URP 中的 _AmbientColor 实际来自 SampleSH,移植版本用一个常量颜色近似。
  • 不支持 HDR 颜色。Unity 的 [HDR] 属性可以给出大于 1 的分量,浏览器的颜色控件只能表达 sRGB 范围内的值。
  • half 精度被放宽为 float。移动端 URP 依赖 half 获得性能,GLSL ES 3.00 里虽然也有 highp/mediump,但为了与桌面驱动行为一致,移植版本统一使用 float。
  • 没有 SRP Batcher。HLSL 中的 CBUFFER_START(UnityPerMaterial) 是为了兼容 SRP Batcher,浏览器版本用普通 uniform 代替,不影响画面。
  • 未在 Unity 中做并排视觉对照,因此“看起来一致”这一条没有被实际验证过。
本页目录

这个示例想说明什么

浏览器不能直接执行 Unity URP 的 HLSL。要在网页上展示一个 URP 效果,只有两条路:把整个工程导出成 Unity WebGL(体积大、加载慢、和静态博客的架构冲突),或者手写一份等价的 GLSL。本站选择第二条,并且坚持一个原则——原始 HLSL 永远保留,移植版本单独存放,两者分开展示,差异写清楚。

原始 HLSL 位于 src/shaders/hlsl/rimlit/,移植后的 GLSL 位于 src/shaders/glsl/rimlit-port.*。上面的「源码」区域会把两者并列出来,浏览器实际编译的只有 GLSL 那一份。

原始 HLSL 的结构

RimLitUnlit.shader 按 URP 的惯例组织:

  • Properties 块声明材质面板上的可编辑属性;
  • SubShader 的 Tags 里写明 "RenderPipeline" = "UniversalPipeline",Pass 的 LightMode 为 UniversalForward;
  • HLSLPROGRAM 中引入 Core.hlsl 与 Lighting.hlsl,材质属性集中在 RimLitUnlitInput.hlsl 的 CBUFFER_START(UnityPerMaterial) 里,以保持 SRP Batcher 兼容;
  • 顶点阶段使用 TransformObjectToWorld / TransformWorldToHClip / TransformObjectToWorldNormal;
  • 片元阶段通过 GetMainLight()(在有阴影关键字时用 GetMainLight(shadowCoord))取得主光方向、颜色与阴影衰减。

光照模型本身很简单:

c=cbase⊙cambient⏟环境+cbase⊙clight ⟨N⋅L⟩ as⏟漫反射+cspec⊙clight ⟨N⋅H⟩2lerp⁡(3,11,s) s⏟高光+crim (1−⟨N⋅V⟩)p I (1+Psin⁡(ωt))⏟边缘光\mathbf{c} = \underbrace{\mathbf{c}_{\text{base}}\odot\mathbf{c}_{\text{ambient}}}_{\text{环境}} + \underbrace{\mathbf{c}_{\text{base}}\odot\mathbf{c}_{\text{light}}\,\langle\mathbf{N}\cdot\mathbf{L}\rangle\,a_s}_{\text{漫反射}} + \underbrace{\mathbf{c}_{\text{spec}}\odot\mathbf{c}_{\text{light}}\,\langle\mathbf{N}\cdot\mathbf{H}\rangle^{2^{\operatorname{lerp}(3,11,s)}}\,s}_{\text{高光}} + \underbrace{\mathbf{c}_{\text{rim}}\,(1-\langle\mathbf{N}\cdot\mathbf{V}\rangle)^{p}\,I\,(1+P\sin(\omega t))}_{\text{边缘光}}

其中 ⟨x⟩=saturate⁡(x)\langle x\rangle = \operatorname{saturate}(x),asa_s 是 shadowAttenuation,PP 是 _RimPulse,ω\omega 是 _PulseSpeed,tt 是 _Time.y。GLSL 版本使用完全相同的表达式,只是把 saturate 写成 clamp(x, 0.0, 1.0)、lerp 写成 mix、half 写成 float。

移植过程中真正麻烦的三件事

1. 光照数据从哪里来

URP 的 GetMainLight() 读取的是引擎每帧填充的 _MainLightPosition / _MainLightColor / _MainLightOcclusionProbes。浏览器里没有这套数据,three.js 也不会提供。所以移植版本必须把它们提升为显式 uniform,交给页面上的控件——这既是妥协,也顺便让访问者可以直接调光。

2. 法线矩阵的语义差异

three.js 提供的 normalMatrix 是 modelViewMatrix 的逆转置,得到视图空间法线;URP 的 TransformObjectToWorldNormal 得到的是世界空间法线。混用会得到「边缘光跟着相机跑」的错误效果。移植版本用 mat3(modelMatrix) 自己算世界空间法线,并在注释里写明:只有在均匀缩放时它才严格等价于逆转置。

3. 输出颜色空间

这是最容易被忽略、但视觉上最明显的一处。Unity 在线性空间项目里输出线性值,由 sRGB framebuffer 在硬件层面完成编码;three.js 对 RawShaderMaterial 不追加任何输出转换代码。如果 GLSL 版本直接输出线性值,画面会明显偏暗。因此移植版本在片元着色器末尾显式调用 linearToSRGB(),并把 renderer.outputColorSpace 设为 LinearSRGBColorSpace 以表明「转换已经在着色器里做完了」。

验证方式与已知不足

  • HLSL:仅静态审查。本仓库没有 Unity 安装,因此没有经过 Unity 的着色器编译器验证,也没有在编辑器里观察过实际效果。这一点在页面顶部的徽标上明确标注为「未在 Unity 中对照验证」。
  • GLSL:真实编译并运行。自动化测试(tests/shader-live.spec.ts)会在无头 Chromium + SwiftShader 中挂载预览,断言状态变为 running、画布出现非纯色像素、修改参数改变画面、暂停后时间停止推进。
  • 逐行对照:人工完成,结果记录在上面的映射表里。

如果你在自己的 Unity 工程里跑过 RimLitUnlit.shader,欢迎对照本页的参数默认值检查差异;把 porting.verifiedInUnity 改为 true 并补充 verificationNotes 之后,页面徽标会自动变成「已在 Unity 中对照验证」。

源码

浏览器 GLSL 与原始 HLSL 分开存放、分开展示。HLSL 仅作参考,浏览器无法直接执行 Unity URP 的 HLSL。

顶点着色器 rimlit-port.vert glsl GLSL ES 3.00 · 浏览器中实际编译 30 行

three.js 会自动在最前面加入 `#version 300 es`,因此源文件本身不包含该指令。

// Browser port of RimLitUnlit.shader (Unity URP / HLSL) - vertex stage.
//
// URP                                  ->  GLSL ES 3.00
// TransformObjectToWorld(positionOS)   ->  (modelMatrix * vec4(position, 1.0)).xyz
// TransformWorldToHClip(positionWS)    ->  projectionMatrix * modelViewMatrix * vec4(position, 1.0)
// TransformObjectToWorldNormal(normal) ->  normalize(mat3(modelMatrix) * normal)
//
// NOTE: `#version 300 es` is injected by three.js (RawShaderMaterial + GLSL3).

precision highp float;

uniform mat4 modelMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

in vec3 position;   // Attributes.positionOS
in vec3 normal;     // Attributes.normalOS
in vec2 uv;         // Attributes.uv

out vec3 vPositionWS;   // Varyings.positionWS
out vec3 vNormalWS;     // Varyings.normalWS
out vec2 vUv;

void main() {
  vec4 worldPosition = modelMatrix * vec4(position, 1.0);
  vPositionWS = worldPosition.xyz;
  vNormalWS = normalize(mat3(modelMatrix) * normal);
  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
片元着色器 rimlit-port.frag glsl GLSL ES 3.00 · 浏览器中实际编译 83 行
// Browser port of RimLitUnlit.shader (Unity URP / HLSL) - fragment stage.
//
// Mapping summary (the full table lives in src/content/shaders/urp-rimlit-port.md):
//   GetMainLight().direction / .color  -> uLightDirection / uLightColor
//   mainLight.shadowAttenuation        -> uShadowAttenuation (exposed as a
//                                         constant: the browser preview has no
//                                         shadow map and no URP light data)
//   _Time.y                            -> uTime (pausable preview clock)
//   _WorldSpaceCameraPos               -> cameraPosition (provided by three.js)
//   half4 color : SV_Target            -> fragColor, plus an explicit
//                                         linear -> sRGB encode, because URP
//                                         relies on an sRGB framebuffer while
//                                         three.js adds no conversion for
//                                         RawShaderMaterial.
//
// NOTE: `#version 300 es` is injected by three.js (RawShaderMaterial + GLSL3).

precision highp float;

in vec3 vPositionWS;
in vec3 vNormalWS;
in vec2 vUv;

uniform vec3  cameraPosition;     // == _WorldSpaceCameraPos
uniform float uTime;              // == _Time.y
uniform vec3  uBaseColor;         // == _BaseColor.rgb      (linear)
uniform vec3  uRimColor;          // == _RimColor.rgb       (linear)
uniform vec3  uSpecularColor;     // == _SpecularColor.rgb  (linear)
uniform vec3  uAmbientColor;      // == _AmbientColor.rgb   (linear)
uniform vec3  uLightColor;        // == GetMainLight().color
uniform vec3  uLightDirection;    // == GetMainLight().direction
uniform float uShadowAttenuation; // == mainLight.shadowAttenuation
uniform float uRimPower;          // == _RimPower
uniform float uRimIntensity;      // == _RimIntensity
uniform float uRimPulse;          // == _RimPulse
uniform float uPulseSpeed;        // == _PulseSpeed
uniform float uSmoothness;        // == _Smoothness

out vec4 fragColor;

vec3 linearToSRGB(vec3 c) {
  vec3 lo = c * 12.92;
  vec3 hi = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;
  return mix(lo, hi, step(vec3(0.0031308), c));
}

// half RimLitShininess(half smoothness)
float rimLitShininess(float smoothness) {
  return exp2(mix(3.0, 11.0, clamp(smoothness, 0.0, 1.0)));
}

// half RimLitFresnel(half3 N, half3 V, half rimPower)
float rimLitFresnel(vec3 N, vec3 V, float rimPower) {
  float cosTheta = clamp(dot(N, V), 0.0, 1.0);
  return pow(1.0 - cosTheta, max(rimPower, 0.0001));
}

void main() {
  vec3 N = normalize(vNormalWS);
  vec3 V = normalize(cameraPosition - vPositionWS);
  vec3 L = normalize(uLightDirection);
  float shadowAtten = clamp(uShadowAttenuation, 0.0, 1.0);

  // half ndotl = saturate(dot(N, L)) * shadowAtten;
  float ndotl = clamp(dot(N, L), 0.0, 1.0) * shadowAtten;
  vec3 diffuse = uBaseColor * uLightColor * ndotl;

  // Blinn-Phong specular.
  vec3 H = normalize(L + V);
  float shininess = rimLitShininess(uSmoothness);
  float ndoth = pow(clamp(dot(N, H), 0.0, 1.0), shininess);
  vec3 specular = uSpecularColor * uLightColor * ndoth * clamp(uSmoothness, 0.0, 1.0);

  // Fresnel rim, optionally pulsing with time.
  float fresnel = rimLitFresnel(N, V, uRimPower);
  float pulse = 1.0 + uRimPulse * sin(uTime * uPulseSpeed);
  vec3 rim = uRimColor * fresnel * uRimIntensity * pulse;

  vec3 ambient = uBaseColor * uAmbientColor;
  vec3 color = ambient + diffuse + specular + rim;

  fragColor = vec4(linearToSRGB(clamp(color, 0.0, 1.0)), 1.0);
}
ShaderLab + HLSLPROGRAM 主体 rimlit/RimLitUnlit.shader hlsl 原始 HLSL · 不在浏览器中执行 133 行
// ---------------------------------------------------------------------------
// Portfolio/RimLitUnlit
//
// Unity URP forward-lit shader: Lambert diffuse + Blinn-Phong specular from the
// main light, plus a Fresnel rim term that can pulse over time.
//
// This is the ORIGINAL HLSL source. The browser cannot execute it; the GLSL ES
// 3.00 port used for the live preview is src/shaders/glsl/rimlit-port.vert and
// src/shaders/glsl/rimlit-port.frag, and the full mapping table plus the list
// of differences is in src/content/shaders/urp-rimlit-port.md.
//
// Verified with: static review only. See "verificationNotes" in the content
// entry - this repository has no Unity installation, so the shader has NOT
// been compiled or visually compared inside the Unity editor.
// ---------------------------------------------------------------------------
Shader "Portfolio/RimLitUnlit"
{
    Properties
    {
        [MainColor] _BaseColor("Base Color", Color) = (0.16, 0.42, 0.75, 1)
        [HDR] _RimColor("Rim Color", Color) = (0.35, 0.95, 1.00, 1)
        _RimPower("Rim Power", Range(0.25, 12)) = 3.0
        _RimIntensity("Rim Intensity", Range(0, 4)) = 1.2
        _RimPulse("Rim Pulse Amount", Range(0, 1)) = 0.0
        _PulseSpeed("Pulse Speed", Range(0, 6)) = 1.5
        _Smoothness("Smoothness", Range(0, 1)) = 0.6
        [HDR] _SpecularColor("Specular Color", Color) = (1, 1, 1, 1)
        _AmbientColor("Ambient Color", Color) = (0.06, 0.07, 0.09, 1)
    }

    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
            "RenderPipeline" = "UniversalPipeline"
            "Queue" = "Geometry"
            "IgnoreProjector" = "True"
        }
        LOD 100

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

            HLSLPROGRAM
            #pragma vertex rimlitVert
            #pragma fragment rimlitFrag
            #pragma target 3.5

            // URP shadow keywords. The browser port has no shadow map, so it
            // always behaves like shadowAttenuation == 1 (documented as a
            // limitation of the port).
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE
            #pragma multi_compile_fragment _ _SHADOWS_SOFT

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

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
                float2 uv         : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float3 positionWS : TEXCOORD0;
                float3 normalWS   : TEXCOORD1;
            };

            Varyings rimlitVert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionWS = TransformObjectToWorld(IN.positionOS.xyz);
                OUT.positionCS = TransformWorldToHClip(OUT.positionWS);
                OUT.normalWS   = TransformObjectToWorldNormal(IN.normalOS);
                return OUT;
            }

            half4 rimlitFrag(Varyings IN) : SV_Target
            {
                float3 N = normalize(IN.normalWS);
                float3 V = normalize(_WorldSpaceCameraPos - IN.positionWS);

            #if defined(_MAIN_LIGHT_SHADOWS) || defined(_MAIN_LIGHT_SHADOWS_CASCADE)
                float4 shadowCoord = TransformWorldToShadowCoord(IN.positionWS);
                Light mainLight = GetMainLight(shadowCoord);
            #else
                Light mainLight = GetMainLight();
            #endif

                float3 L = normalize(mainLight.direction);
                half3  lightColor = mainLight.color;
                half   shadowAtten = mainLight.shadowAttenuation;

                // Lambert diffuse.
                half ndotl = saturate(dot(N, L)) * shadowAtten;
                half3 diffuse = _BaseColor.rgb * lightColor * ndotl;

                // Blinn-Phong specular.
                half3 H = normalize(L + V);
                half shininess = RimLitShininess(_Smoothness);
                half ndoth = pow(saturate(dot(N, H)), shininess);
                half3 specular = _SpecularColor.rgb * lightColor * ndoth * saturate(_Smoothness);

                // Fresnel rim, optionally pulsing with _Time.y.
                half fresnel = RimLitFresnel(N, V, _RimPower);
                half pulse = 1.0h + _RimPulse * sin(_Time.y * _PulseSpeed);
                half3 rim = _RimColor.rgb * fresnel * _RimIntensity * pulse;

                half3 ambient = _BaseColor.rgb * _AmbientColor.rgb;

                // In a linear-colour-space project Unity converts the sRGB
                // values from the colour picker to linear before they reach
                // this shader, and converts the result back to sRGB when
                // writing to the framebuffer. Neither conversion appears here.
                half3 color = ambient + diffuse + specular + rim;
                return half4(color, _BaseColor.a);
            }
            ENDHLSL
        }

        // ShadowCaster / DepthOnly / DepthNormalsOnly passes are intentionally
        // omitted from this reference implementation.
    }

    FallBack "Universal Render Pipeline/Unlit"
}
材质属性 CBUFFER 与工具函数 rimlit/RimLitUnlitInput.hlsl hlsl 原始 HLSL · 不在浏览器中执行 42 行
#ifndef RIMLIT_UNLIT_INPUT_INCLUDED
#define RIMLIT_UNLIT_INPUT_INCLUDED

// ---------------------------------------------------------------------------
// Portfolio/RimLitUnlit - material property declarations.
//
// Unity URP (HLSL). This file is the ORIGINAL source and is never compiled by
// the browser. The GLSL ES 3.00 port lives in src/shaders/glsl/rimlit-port.*.
//
// The whole material must sit inside one CBUFFER named UnityPerMaterial so the
// shader stays SRP-Batcher compatible.
// ---------------------------------------------------------------------------

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"

CBUFFER_START(UnityPerMaterial)
    half4 _BaseColor;
    half4 _RimColor;
    half4 _SpecularColor;
    half4 _AmbientColor;
    half  _RimPower;
    half  _RimIntensity;
    half  _RimPulse;
    half  _PulseSpeed;
    half  _Smoothness;
CBUFFER_END

// Perceptual smoothness -> Blinn-Phong exponent. Both sides of the port use
// this exact expression, which is what makes the specular highlight match.
half RimLitShininess(half smoothness)
{
    return exp2(lerp(3.0h, 11.0h, saturate(smoothness)));
}

// Schlick-style rim term: pow(1 - saturate(N dot V), power).
half RimLitFresnel(half3 N, half3 V, half rimPower)
{
    half cosTheta = saturate(dot(N, V));
    return pow(1.0h - cosTheta, max(rimPower, 0.0001h));
}

#endif // RIMLIT_UNLIT_INPUT_INCLUDED