【Unity Shader URP】全息扫描线(Hologram Scanline)源码+脚本控制

文章目录

    • [0. 效果预览](#0. 效果预览)
    • [1. 原理简述](#1. 原理简述)
    • [2. 功能点](#2. 功能点)
    • [3. 完整 Shader(可直接用)](#3. 完整 Shader(可直接用))
    • [4. 使用方法](#4. 使用方法)
    • [5. 参数说明](#5. 参数说明)
    • [6. 变体与扩展](#6. 变体与扩展)
      • [6.1 双层扫描线(粗线 + 细线)](#6.1 双层扫描线(粗线 + 细线))
      • [6.2 顶点抖动(信号干扰感)](#6.2 顶点抖动(信号干扰感))
      • [6.3 主贴图叠加](#6.3 主贴图叠加)
    • [7. 常见问题](#7. 常见问题)
    • [8. 性能建议](#8. 性能建议)

0. 效果预览

全息扫描线是科幻游戏/UI 中的经典视觉效果:半透明的模型表面叠加水平滚动的扫描亮线,配合菲涅尔边缘光,营造出全息投影的未来感。常用于全息通讯、敌人扫描、科幻 UI 面板等场景。


1. 原理简述

全息扫描线的本质:用物体的世界空间 Y 坐标做周期性 sin/frac 运算,生成水平条纹,再让条纹随时间滚动。

关键伪代码:

hlsl 复制代码
float worldY = positionWS.y;
// 用 frac 生成 0~1 重复的锯齿波,乘以密度控制条纹数量
float scanline = frac(worldY * _LinesDensity + _Time.y * _ScrollSpeed);
// step 把锯齿波二值化:低于阈值的部分变成亮线
scanline = step(scanline, _LinesWidth);

整个效果由三层叠加:

  1. 半透明底色:全息主色调(青色/蓝色),整体半透明
  2. 扫描线:世界空间 Y 方向的水平条纹,随时间滚动
  3. 菲涅尔边缘光:边缘比中心更亮,增强立体感和全息感

2. 功能点

  • 世界空间扫描线:基于世界 Y 坐标生成水平条纹,不受模型 UV 影响,多个模型的扫描线能自然对齐
  • 滚动动画 :扫描线随 _Time 自动向上滚动,速度可控
  • 条纹密度与宽度:独立控制扫描线的密度和每条线的粗细
  • 菲涅尔边缘光:视角边缘更亮,模拟全息投影的发光边沿
  • 半透明渲染Blend SrcAlpha OneMinusSrcAlpha,可叠加在场景中
  • 全息主色调:统一的 HDR 颜色控制整体色调
  • 闪烁效果:可选的全局闪烁,模拟全息信号不稳定

3. 完整 Shader(可直接用)

hlsl 复制代码
Shader "Custom/HologramScanline_URP"
{
    Properties
    {
        // 全息主色调(建议 HDR,如青色 (0, 2, 3),配合 Bloom 效果更好)
        [HDR] _HoloColor ("Hologram Color", Color) = (0, 1.5, 2, 1)
        // 整体透明度(0 = 完全透明,1 = 完全不透明)
        _Alpha ("Alpha", Range(0, 1)) = 0.5

        // ===== 扫描线参数 =====
        // 扫描线密度(值越大条纹越密)
        _LinesDensity ("Lines Density", Float) = 60.0
        // 扫描线宽度(占一个周期的比例,越小线越细)
        _LinesWidth ("Lines Width", Range(0.0, 1.0)) = 0.1
        // 扫描线亮度增益
        _LinesIntensity ("Lines Intensity", Range(0, 3)) = 1.0
        // 扫描线滚动速度(正值向上,负值向下)
        _ScrollSpeed ("Scroll Speed", Float) = 1.0

        // ===== 菲涅尔参数 =====
        // 菲涅尔强度(值越大,边缘光越集中在边沿)
        _FresnelPower ("Fresnel Power", Range(0.1, 10)) = 2.0
        // 菲涅尔亮度增益
        _FresnelIntensity ("Fresnel Intensity", Range(0, 5)) = 1.5

        // ===== 闪烁参数 =====
        // 闪烁速度(0 = 不闪烁)
        _FlickerSpeed ("Flicker Speed", Float) = 3.0
        // 闪烁幅度(0~1,0 = 无闪烁,1 = 完全明灭)
        _FlickerAmount ("Flicker Amount", Range(0, 1)) = 0.1
    }

    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalRenderPipeline"
            "Queue" = "Transparent"           // 半透明队列
            "RenderType" = "Transparent"
        }

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

            Cull Back           // 只渲染正面(全息效果一般不需要背面)
            ZWrite Off          // 半透明物体关深度写入,避免遮挡问题
            Blend SrcAlpha OneMinusSrcAlpha   // 标准 Alpha 混合

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            // GPU Instancing 支持
            #pragma multi_compile_instancing

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

            // =========================================================
            // 材质属性(与 Properties 一一对应)
            // =========================================================
            float4 _HoloColor;
            float  _Alpha;

            float  _LinesDensity;
            float  _LinesWidth;
            float  _LinesIntensity;
            float  _ScrollSpeed;

            float  _FresnelPower;
            float  _FresnelIntensity;

            float  _FlickerSpeed;
            float  _FlickerAmount;

            struct Attributes
            {
                float4 positionOS : POSITION;   // 模型空间顶点位置
                float3 normalOS   : NORMAL;     // 模型空间法线(菲涅尔需要)
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;  // 裁剪空间位置
                float3 positionWS  : TEXCOORD0;    // 世界空间位置(扫描线用)
                float3 normalWS    : TEXCOORD1;    // 世界空间法线(菲涅尔用)
                float3 viewDirWS   : TEXCOORD2;    // 世界空间视线方向(菲涅尔用)
                UNITY_VERTEX_INPUT_INSTANCE_ID
                UNITY_VERTEX_OUTPUT_STEREO
            };

            // =========================================================
            // 顶点着色器:计算世界空间位置、法线、视线方向
            // =========================================================
            Varyings vert(Attributes v)
            {
                Varyings o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

                // 变换到各个空间
                o.positionHCS = TransformObjectToHClip(v.positionOS.xyz);
                o.positionWS  = TransformObjectToWorld(v.positionOS.xyz);
                o.normalWS    = TransformObjectToWorldNormal(v.normalOS);

                // 视线方向 = 相机位置 - 顶点世界位置
                o.viewDirWS = GetWorldSpaceNormalizeViewDir(o.positionWS);

                return o;
            }

            // =========================================================
            // 片元着色器:扫描线 + 菲涅尔 + 闪烁
            // =========================================================
            half4 frag(Varyings i) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(i);

                // ---------- 1) 扫描线 ----------
                // 用世界 Y 坐标生成条纹,不依赖 UV,多物体能对齐
                float scanY = i.positionWS.y * _LinesDensity + _Time.y * _ScrollSpeed;
                // frac 取小数部分 → 0~1 锯齿波
                float scanline = frac(scanY);
                // step 二值化:小于 _LinesWidth 的部分 = 1(亮线),其余 = 0
                scanline = step(scanline, _LinesWidth) * _LinesIntensity;

                // ---------- 2) 菲涅尔边缘光 ----------
                float3 N = normalize(i.normalWS);
                float3 V = normalize(i.viewDirWS);
                // 经典菲涅尔:1 - NdotV,边缘 NdotV 接近 0,所以 fresnel 接近 1
                float NdotV = saturate(dot(N, V));
                float fresnel = pow(1.0 - NdotV, _FresnelPower) * _FresnelIntensity;

                // ---------- 3) 闪烁 ----------
                // sin 产生 -1~1 波动,映射到 1-amount ~ 1 范围
                float flicker = 1.0 - _FlickerAmount * 0.5 * (1.0 + sin(_Time.y * _FlickerSpeed));

                // ---------- 4) 合成 ----------
                // 基础颜色 = 全息色 × (扫描线亮度 + 菲涅尔亮度 + 基础底色)
                half3 color = _HoloColor.rgb * (scanline + fresnel + 0.3);
                // 闪烁影响整体亮度
                color *= flicker;

                // 透明度:边缘更亮也更不透明,中心更透
                float alpha = _Alpha * (fresnel + scanline * 0.5 + 0.3);
                alpha = saturate(alpha) * flicker;

                return half4(color, alpha);
            }
            ENDHLSL
        }
    }
}

4. 使用方法

  1. 在 Unity 项目的 Assets/Shaders/ 下新建文件 HologramScanline_URP.shader,粘贴上方完整代码。
  2. 新建材质(Create → Material),Shader 选择 Custom/HologramScanline_URP
  3. 将材质赋给场景中的模型(推荐用人形模型或几何体,效果更明显)。
  4. 在 Inspector 中调节参数:
    • Hologram Color:推荐青色系 HDR 值,如 (0, 1.5, 2)(0, 3, 4)
    • Lines Density:60~150 之间尝试,值越大扫描线越密
    • Lines Width:0.05~0.2,控制每条线的粗细
    • Scroll Speed:1~3,控制滚动快慢
  5. 开启 URP 的 Bloom 后处理效果,全息的 HDR 颜色会产生辉光,效果大幅提升。

用脚本控制全息显隐(如扫描敌人时触发):

csharp 复制代码
using UnityEngine;

public class HologramToggle : MonoBehaviour
{
    [SerializeField] private Material holoMaterial;
    [SerializeField] private float fadeDuration = 0.5f;

    private float _targetAlpha;
    private float _currentAlpha;

    // 外部调用:显示全息
    public void ShowHologram()
    {
        _targetAlpha = 0.5f;
    }

    // 外部调用:隐藏全息
    public void HideHologram()
    {
        _targetAlpha = 0f;
    }

    void Update()
    {
        _currentAlpha = Mathf.MoveTowards(_currentAlpha, _targetAlpha,
            Time.deltaTime / fadeDuration);
        holoMaterial.SetFloat("_Alpha", _currentAlpha);
    }
}

5. 参数说明

参数 类型 范围/默认值 说明
_HoloColor Color [HDR] (0, 1.5, 2, 1) 全息主色调,建议 HDR 值配合 Bloom
_Alpha Range(0,1) 0.5 整体透明度
_LinesDensity Float 60.0 扫描线密度,值越大条纹越密
_LinesWidth Range(0,1) 0.1 扫描线宽度,占一个周期的比例
_LinesIntensity Range(0,3) 1.0 扫描线亮度增益
_ScrollSpeed Float 1.0 滚动速度,正值向上,负值向下
_FresnelPower Range(0.1,10) 2.0 菲涅尔指数,越大边缘光越窄
_FresnelIntensity Range(0,5) 1.5 菲涅尔亮度增益
_FlickerSpeed Float 3.0 闪烁频率,0 = 不闪烁
_FlickerAmount Range(0,1) 0.1 闪烁幅度,越大明灭越剧烈

6. 变体与扩展

6.1 双层扫描线(粗线 + 细线)

真实全息投影通常有多层条纹叠加,加一组低密度粗线增强层次感:

hlsl 复制代码
// 在 frag 中,原有 scanline 之后追加:
float scanCoarse = frac(i.positionWS.y * _LinesDensity * 0.15 + _Time.y * _ScrollSpeed * 0.5);
scanCoarse = step(scanCoarse, 0.03) * 2.0;  // 粗线更亮、更稀疏
scanline += scanCoarse;

6.2 顶点抖动(信号干扰感)

给顶点加一点水平方向的随机偏移,模拟全息信号不稳定:

hlsl 复制代码
// 在 vert 中,TransformObjectToHClip 之前:
float glitch = step(0.95, frac(sin(v.positionOS.y * 50.0 + _Time.y * 20.0) * 43758.5453));
v.positionOS.x += glitch * 0.02;  // 偶尔水平偏移一小段

6.3 主贴图叠加

如果需要全息投影上显示纹理细节(如角色衣服花纹),可以加一张贴图:

hlsl 复制代码
// Properties 中增加:
_BaseMap ("Base Map", 2D) = "white" {}

// 声明贴图和采样器:
TEXTURE2D(_BaseMap);  SAMPLER(sampler_BaseMap);
float4 _BaseMap_ST;

// Attributes 中增加 UV:
float2 uv : TEXCOORD0;

// Varyings 中增加 UV:
float2 uv : TEXCOORD3;

// vert 中:
o.uv = TRANSFORM_TEX(v.uv, _BaseMap);

// frag 合成时叠加贴图灰度:
float texDetail = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv).r;
color *= (0.5 + texDetail * 0.5);  // 让贴图明暗影响全息亮度

7. 常见问题

Q: 扫描线在模型上是斜的或者弯曲的?

A: 扫描线基于世界空间 Y 坐标,所以它始终是水平的。如果看起来不水平,检查模型是否旋转了。如果需要扫描线跟随模型旋转,改用模型空间坐标 v.positionOS.y(在 vert 中传给 Varyings)。

Q: 开了 Bloom 但全息没有辉光效果?

A: 检查两点:① _HoloColor 的 RGB 分量需要大于 1(HDR 值),比如 (0, 3, 4) 而不是 (0, 0.5, 0.8)。② URP Asset 中确认勾选了 HDR,并且 Post-processing 的 Bloom 已添加到 Volume 中。

Q: 模型完全看不见 / 完全透明?

A: 检查 _Alpha 是否太小。另外确认 URP Renderer 的 Transparent 排序没有问题------在 Universal Renderer Data 中确保 Transparent Layer Mask 包含了物体所在 Layer。

Q: 扫描线滚动有卡顿/跳帧感?

A: _ScrollSpeed 过大时,条纹移动过快会产生视觉频闪(类似车轮倒转错觉)。降低速度或增加 _LinesDensity 可以缓解。

Q: 想让全息效果只出现在模型的一部分(比如上半身)?

A: 在 frag 中加一个高度裁剪:clip(i.positionWS.y - _CutoffY);,用 _CutoffY 控制从哪个高度开始显示。也可以用一张 Mask 贴图控制更精细的区域。


8. 性能建议

  • 简单数学 :整个效果只用了 fracsteppowsin,没有纹理采样(除非加变体),片元开销极低。
  • 关闭深度写入ZWrite Off 是半透明必须的,但也意味着全息物体不会遮挡后面的物体。如果场景中有大量全息物体互相重叠,注意 Overdraw。
  • 远距离简化 :远处全息扫描线太密会变成噪点,可以用脚本根据距离降低 _LinesDensity,或者直接隐藏全息层换成简单的半透明色。
  • Bloom 开销 :Bloom 后处理是全息效果的重要组成部分,但它是全屏后处理。如果只有少量全息物体,Bloom 的性价比不高------可以考虑在 Shader 中用 smoothstep 模拟柔和发光,省掉 Bloom 的全屏 Pass。
  • Instancing :多个全息物体使用同一材质时,GPU Instancing 可以合批渲染。确保材质属性用 MaterialPropertyBlock 设置而非直接修改 material,否则会打断合批。
相关推荐
玖釉-4 小时前
图形 API 的前沿试车场:Vulkan 扩展体系深度解析与引擎架构实践
c++·架构·图形渲染
玖釉-4 小时前
告别 Shared Memory 瓶颈:Vulkan Subgroup 架构解析与硬核实战指南
开发语言·c++·windows·图形渲染
渔民小镇5 小时前
一次编写到处对接 —— 为 Godot/Unity/React 生成统一交互接口
java·分布式·游戏·unity·godot
小小工匠15 小时前
LLM - awesome-design-md 从 DESIGN.md 到“可对话的设计系统”:用纯文本驱动 AI 生成一致 UI 的新范式
人工智能·ui
RReality18 小时前
【Unity Shader URP】序列帧动画(Sprite Sheet)实战教程
unity·游戏引擎
mxwin18 小时前
Unity URP 多线程渲染:理解 Shader 变体对加载时间的影响
unity·游戏引擎·shader
Ulyanov19 小时前
基于ttk的现代化Python音视频播放器:UI设计与可视化技术深度解析
python·ui·音视频
呆呆敲代码的小Y20 小时前
【Unity工具篇】| 游戏完整资源热更新流程,YooAsset官方示例项目
人工智能·游戏·unity·游戏引擎·热更新·yooasset·免费游戏
nainaire20 小时前
自学虚幻引擎记录1
游戏引擎·虚幻