GameTech/URP 渲染实战
源码 / Ch19/Editor/Ch19Shots.cs
Ch19/Editor/Ch19Shots.cs
// 第 19 章配图:一个小水池场景,水面材质按案例逐步打开各个特性。
using Cookbook.Core;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace Cookbook.Ch19
{
    static class Shots
    {
        // ---------------- 场景 ----------------
        // 水池地形:中间深、左后方是缓坡沙滩,高度场网格
        static float BasinHeight(float x, float z)
        {
            float s = Mathf.SmoothStep(0, 1, Mathf.InverseLerp(1.0f, 6.5f, -x * 0.75f + z * 0.65f));
            float h = Mathf.Lerp(-1.7f, 0.7f, s);
            h += 0.12f * Mathf.Sin(x * 1.3f + 0.5f) * Mathf.Sin(z * 1.1f);
            return h;
        }

        static Mesh BasinMesh(float size = 60f, int n = 300)
        {
            var v = new Vector3[(n + 1) * (n + 1)];
            var uv = new Vector2[v.Length];
            for (int z = 0; z <= n; z++)
                for (int x = 0; x <= n; x++)
                {
                    float px = ((float)x / n - 0.5f) * size, pz = ((float)z / n - 0.5f) * size;
                    v[z * (n + 1) + x] = new Vector3(px, BasinHeight(px, pz), pz);
                    uv[z * (n + 1) + x] = new Vector2(px, pz) * 0.5f;
                }
            var idx = new int[n * n * 6]; int p = 0;
            for (int z = 0; z < n; z++)
                for (int x = 0; x < n; x++)
                {
                    int a = z * (n + 1) + x, b = a + n + 1;
                    idx[p++] = a; idx[p++] = b; idx[p++] = a + 1;
                    idx[p++] = a + 1; idx[p++] = b; idx[p++] = b + 1;
                }
            var m = new Mesh { indexFormat = IndexFormat.UInt32, vertices = v, uv = uv, triangles = idx };
            m.RecalculateNormals(); m.RecalculateTangents(); m.RecalculateBounds();
            return m;
        }

        static GameObject Rock(Shot s, Material m, Vector3 pos, Vector3 scale, float yaw)
        {
            var go = s.Sphere(m, pos);
            go.transform.localScale = scale;
            go.transform.rotation = Quaternion.Euler(8, yaw, -6);
            return go;
        }

        static void Pool(Shot s)
        {
            var sand = s.Lit(new Color(0.9f, 0.8f, 0.62f), 0, 0.15f);
            sand.SetTexture("_BaseMap", s.CoreTex("checker"));
            s.MeshObj(BasinMesh(), sand, Vector3.zero, 1, "Basin");

            var rock = s.Lit(new Color(0.42f, 0.42f, 0.45f), 0, 0.25f);
            rock.SetTexture("_BaseMap", s.CoreTex("noise_fbm"));
            Rock(s, rock, new Vector3(-1.2f, -0.55f, 0.6f), new Vector3(1.6f, 0.9f, 1.3f), 20);   // 水下暗礁,顶部离水面很近
            Rock(s, rock, new Vector3(2.6f, -0.35f, 2.4f), new Vector3(2.0f, 1.3f, 1.6f), -30);   // 露出水面的大石头
            Rock(s, rock, new Vector3(-3.4f, 0.1f, 3.2f), new Vector3(1.2f, 0.8f, 1.0f), 60);     // 沙滩上的石头

            var stone = s.Lit(new Color(0.75f, 0.72f, 0.68f), 0, 0.3f);
            foreach (var px in new[] { new Vector3(0.9f, 0, 1.4f), new Vector3(-0.3f, 0, 3.6f), new Vector3(3.6f, 0, -0.4f) })
            {
                var c = s.Cylinder(stone, new Vector3(px.x, -0.1f, px.z));
                c.transform.localScale = new Vector3(0.45f, 1.6f, 0.45f);                         // 柱子:从水底一直伸出水面
            }

            // 木栈道:前景横跨水面,用来演示折射的“前景泄漏”和平面反射
            var wood = s.Lit(new Color(0.45f, 0.3f, 0.18f), 0, 0.3f);
            var deck = s.Cube(wood, new Vector3(-2.2f, 0.32f, -1.2f));
            deck.transform.localScale = new Vector3(4.0f, 0.12f, 0.9f);
            deck.transform.rotation = Quaternion.Euler(0, 25, 0);
            foreach (var dx in new[] { -1.6f, 0f, 1.6f })
            {
                var post = s.Cylinder(wood, deck.transform.TransformPoint(new Vector3(dx / 4f, 0, 0)) + Vector3.down * 0.9f);
                post.transform.localScale = new Vector3(0.14f, 0.95f, 0.14f);
            }
        }

        static void DefaultView(Shot s) => s.LookAt(new Vector3(0.6f, 2.5f, -6.6f), new Vector3(0, -0.3f, 1.2f), 45);

        static Material WaterMat(Shot s)
        {
            var m = s.Mat("Cookbook/Ch19/Water");
            // 用程序生成的可平铺法线(见案例 19.3,公共贴图 water_normal 按同样规则生成,也可直接换用)
            m.SetTexture("_NormalMap", WaterNormalGenerator.Create());
            m.SetTexture("_FoamNoise", s.CoreTex("noise_fbm"));
            m.SetFloat("_Phase", 3.0f);
            return m;
        }

        // 只保留 on 里列出的特性
        static void Features(Material m, params string[] on)
        {
            string[] all = { "_REFRACTION", "_NORMALS", "_REFLECTION", "_PLANAR_REFLECTION", "_SPECULAR", "_FOAM", "_CAUSTICS", "_GERSTNER" };
            foreach (var k in all)
            {
                bool e = System.Array.IndexOf(on, k) >= 0;
                if (e) m.EnableKeyword(k); else m.DisableKeyword(k);
                // 同步 Toggle 属性,便于在检视面板里看出当前状态
                string prop = k == "_PLANAR_REFLECTION" ? "_PlanarReflection" : "_" + k.Substring(1, 1) + k.Substring(2).ToLowerInvariant();
                if (m.HasProperty(prop)) m.SetFloat(prop, e ? 1 : 0);
            }
        }

        static GameObject Water(Shot s, Material m, int n = 240, float size = 60f)
        {
            var go = s.Grid(m, new Vector3(0, 0, 0), size, n);
            go.layer = 4;     // Water 层:平面反射相机不渲染它
            return go;
        }

        static void Glow(Shot s, float bloom = 0.6f)
        {
            var p = s.Post();
            var b = p.Add<Bloom>(); b.intensity.Override(bloom); b.threshold.Override(1.1f); b.scatter.Override(0.6f);
            var t = p.Add<Tonemapping>(); t.mode.Override(TonemappingMode.Neutral);
        }

        // ---------------- 19.1 场景与深度 ----------------
        [BookShot("ch19-pool", W = 640, H = 420, Panels = 2)]
        static void PoolShot(Shot s)
        {
            Pool(s); DefaultView(s);
            if (s.Panel == 1)
            {
                // 最朴素的水:单色半透明
                var m = WaterMat(s); Features(m);
                m.SetColor("_ShallowColor", new Color(0.15f, 0.45f, 0.6f, 0.6f));
                m.SetColor("_DeepColor", new Color(0.15f, 0.45f, 0.6f, 0.6f));
                m.SetFloat("_EdgeFade", 0.001f);
                Water(s, m);
            }
        }

        [BookShot("ch19-depth", W = 640, H = 420, Panels = 3)]
        static void Depth(Shot s)
        {
            Pool(s); DefaultView(s);
            var m = WaterMat(s); Features(m);
            if (s.Panel == 0) m.SetFloat("_DebugView", 1);
            if (s.Panel == 1) m.SetFloat("_DebugView", 2);
            Water(s, m);
        }

        // ---------------- 19.2 折射 ----------------
        [BookShot("ch19-refraction", W = 640, H = 420, Panels = 3)]
        static void Refraction(Shot s)
        {
            Pool(s);
            // 一个悬在水面上方的橙色箱子:它完全不在水里,水面上却“折射”出橙色,就是前景泄漏
            s.Cube(s.Lit(new Color(1f, 0.45f, 0.1f), 0, 0.4f), new Vector3(0.2f, 0.45f, -2.6f), 0.5f)
             .transform.rotation = Quaternion.Euler(0, 30, 0);
            s.LookAt(new Vector3(0.6f, 1.5f, -4.6f), new Vector3(0.0f, 0.0f, -1.8f), 40);
            var m = WaterMat(s); Features(m, "_REFRACTION", "_NORMALS");
            m.SetVector("_FlowB", new Vector4(0, 0, 0, 0));       // 先只用一层法线
            m.SetVector("_FlowA", new Vector4(0.03f, 0.02f, 1.0f, 0));
            m.SetFloat("_RefractionStrength", s.Panel == 0 ? 0f : 0.2f);
            m.SetFloat("_RefractLeakFix", s.Panel == 2 ? 1 : 0);
            Water(s, m);
        }

        // ---------------- 19.3 双法线 ----------------
        [BookShot("ch19-normals", W = 640, H = 420, Panels = 3)]
        static void Normals(Shot s)
        {
            Pool(s); DefaultView(s);
            var m = WaterMat(s); Features(m, "_REFRACTION", "_NORMALS", "_REFLECTION");
            if (s.Panel == 0) { m.SetVector("_FlowB", Vector4.zero); m.SetFloat("_DebugView", 3); }
            if (s.Panel == 1) { m.SetFloat("_DebugView", 3); }
            Water(s, m);
        }

        // ---------------- 19.4 菲涅尔与反射 ----------------
        [BookShot("ch19-fresnel", W = 640, H = 420, Panels = 3)]
        static void Fresnel(Shot s)
        {
            Pool(s); DefaultView(s);
            var m = WaterMat(s);
            if (s.Panel == 0) Features(m, "_REFRACTION", "_NORMALS");
            else Features(m, "_REFRACTION", "_NORMALS", "_REFLECTION");
            if (s.Panel == 1) { m.SetFloat("_UseFresnel", 0); m.SetFloat("_ReflectionStrength", 0.5f); }
            Water(s, m);
        }

        // ---------------- 19.5 高光 ----------------
        [BookShot("ch19-specular", W = 640, H = 420, Panels = 3)]
        static void Specular(Shot s)
        {
            Pool(s);
            s.SunDir(18, 175, 1.5f);     // 太阳在相机对面低角度:水面上拉出一条亮带
            s.LookAt(new Vector3(0.6f, 2.0f, -6.6f), new Vector3(0, 0.0f, 2.0f), 45);
            var m = WaterMat(s);
            if (s.Panel == 0) Features(m, "_REFRACTION", "_NORMALS", "_REFLECTION");
            else Features(m, "_REFRACTION", "_NORMALS", "_REFLECTION", "_SPECULAR");
            if (s.Panel == 1) m.SetFloat("_SpecPower", 120);
            if (s.Panel == 2) m.SetFloat("_SpecPower", 1200);
            Water(s, m);
        }

        // ---------------- 19.6 泡沫 ----------------
        [BookShot("ch19-foam", W = 640, H = 420, Panels = 3)]
        static void Foam(Shot s)
        {
            Pool(s);
            s.LookAt(new Vector3(-1.0f, 2.4f, -2.6f), new Vector3(-2.0f, -0.2f, 2.4f), 45);
            var m = WaterMat(s);
            if (s.Panel == 0) Features(m, "_REFRACTION", "_NORMALS", "_REFLECTION", "_SPECULAR");
            else Features(m, "_REFRACTION", "_NORMALS", "_REFLECTION", "_SPECULAR", "_FOAM");
            if (s.Panel == 1) m.SetFloat("_DebugView", 4);
            Water(s, m);
        }

        // ---------------- 19.7 焦散 ----------------
        [BookShot("ch19-caustics", W = 640, H = 420, Panels = 3)]
        static void Caustics(Shot s)
        {
            Pool(s);
            s.LookAt(new Vector3(-0.6f, 2.6f, -3.4f), new Vector3(-0.9f, -0.6f, 0.9f), 45);
            var m = WaterMat(s);
            if (s.Panel == 0) { Features(m, "_REFRACTION", "_NORMALS"); m.SetFloat("_DebugView", 5); }
            if (s.Panel == 1) Features(m, "_REFRACTION", "_NORMALS", "_REFLECTION", "_SPECULAR", "_FOAM");
            if (s.Panel == 2) Features(m, "_REFRACTION", "_NORMALS", "_REFLECTION", "_SPECULAR", "_FOAM", "_CAUSTICS");
            Water(s, m);
        }

        // ---------------- 19.8 Gerstner 波 ----------------
        [BookShot("ch19-gerstner", W = 640, H = 420, Panels = 3)]
        static void Gerstner(Shot s)
        {
            Pool(s);
            s.LookAt(new Vector3(0.6f, 1.5f, -6.6f), new Vector3(0, 0.0f, 2.0f), 45);
            var m = WaterMat(s);
            if (s.Panel == 0) Features(m, "_REFRACTION", "_NORMALS", "_REFLECTION", "_SPECULAR", "_FOAM", "_CAUSTICS");
            else Features(m, "_REFRACTION", "_NORMALS", "_REFLECTION", "_SPECULAR", "_FOAM", "_CAUSTICS", "_GERSTNER");
            if (s.Panel == 1) { m.SetVector("_WaveB", new Vector4(1, 0, 0, 1)); m.SetVector("_WaveC", new Vector4(1, 0, 0, 1)); m.SetVector("_WaveA", new Vector4(1, 0.3f, 0.28f, 6)); }
            Water(s, m, 400);
        }

        // ---------------- 19.9 平面反射 ----------------
        [BookShot("ch19-planar", W = 640, H = 420, Panels = 2)]
        static void Planar(Shot s)
        {
            Pool(s);
            s.LookAt(new Vector3(0.6f, 1.2f, -6.6f), new Vector3(0, 0.2f, 2.0f), 45);
            var m = WaterMat(s);
            if (s.Panel == 0) Features(m, "_REFRACTION", "_NORMALS", "_REFLECTION", "_SPECULAR", "_FOAM", "_CAUSTICS");
            else Features(m, "_REFRACTION", "_NORMALS", "_REFLECTION", "_PLANAR_REFLECTION", "_SPECULAR", "_FOAM", "_CAUSTICS");
            var w = Water(s, m);
            if (s.Panel == 1) AddPlanar(s, w, 640, 420);
        }

        // 拍摄时没有 LateUpdate:在主相机渲染之前手动提交一次反射渲染请求
        static void AddPlanar(Shot s, GameObject water, int w, int h)
        {
            var pr = water.AddComponent<PlanarReflection>();
            pr.updateMode = PlanarReflection.UpdateMode.Manual;
            pr.resolutionScale = 1f;
            var tmp = RenderTexture.GetTemporary(w, h, 24);
            s.Cam.targetTexture = tmp;
            s.Cam.aspect = (float)w / h;
            pr.RenderFor(s.Cam);
            s.Cam.targetTexture = null;
            RenderTexture.ReleaseTemporary(tmp);
        }

        // ---------------- 19.10 最终合成 ----------------
        static void Final(Shot s, Material m)
        {
            Features(m, "_REFRACTION", "_NORMALS", "_REFLECTION", "_PLANAR_REFLECTION", "_SPECULAR", "_FOAM", "_CAUSTICS", "_GERSTNER");
            // 最终版用更平缓的波:小水池里浪太大会漫上沙滩
            m.SetVector("_WaveA", new Vector4(1, 0.3f, 0.07f, 7));
            m.SetVector("_WaveB", new Vector4(0.4f, 1, 0.06f, 4.3f));
            m.SetVector("_WaveC", new Vector4(-0.7f, 0.6f, 0.05f, 2.6f));
            m.SetFloat("_SpecIntensity", 0.25f);
            m.SetFloat("_SpecPower", 2048);
            m.SetFloat("_CausticIntensity", 0.9f);
            AddPlanar(s, Water(s, m, 400), 960, 560);
            Glow(s);
        }

        [BookShot("ch19-final", Hdr = true, W = 960, H = 560)]
        static void FinalWide(Shot s)
        {
            Pool(s);
            s.SunDir(30, 170, 1.5f);
            s.LookAt(new Vector3(1.2f, 2.2f, -6.8f), new Vector3(-0.4f, -0.2f, 1.6f), 48);
            Final(s, WaterMat(s));
        }

        [BookShot("ch19-final-close", Hdr = true, W = 960, H = 560)]
        static void FinalClose(Shot s)
        {
            Pool(s);
            s.SunDir(40, -60, 1.4f);
            s.LookAt(new Vector3(0.4f, 1.5f, -1.5f), new Vector3(-1.3f, -0.4f, 1.4f), 50);
            Final(s, WaterMat(s));
        }
    }
}