源码 / Ch20/Editor/Ch20Shots.cs
Ch20/Editor/Ch20Shots.cs
// 第 20 章配图 using System.Collections.Generic; using Cookbook.Core; using UnityEngine; namespace Cookbook.Ch20 { static class Ch20Shots { static float Smooth(float a, float b, float x) { float t = Mathf.Clamp01((x - a) / (b - a)); return t * t * (3 - 2 * t); } /// 小块台地:中间一圈陡崖,用来看拉伸 static float MesaHeight(float x, float z) { float r = Mathf.Sqrt(x * x + (z - 1.5f) * (z - 1.5f)); float edge = r + (Heightfield.Fbm(x * 0.35f + 3, z * 0.35f + 1, 3) - 0.5f) * 2.2f; float mesa = 2.4f * Smooth(3.6f, 2.9f, edge); float bumps = (Heightfield.Fbm(x * 0.25f, z * 0.25f, 4) - 0.5f) * 0.8f; return mesa + bumps; } static GameObject Terrain(Shot s, Material m, float size, int n, System.Func<float, float, float> h) { var go = s.MeshObj(Heightfield.Build(size, n, h), m, Vector3.zero, 1, "Terrain"); return go; } static Material AutoTerrainMat(Shot s) { var m = s.Mat("Cookbook/Ch20/AutoTerrain"); m.SetTexture("_GrassTex", TerrainTextures.Grass); m.SetTexture("_SandTex", TerrainTextures.Sand); m.SetTexture("_RockTex", TerrainTextures.Rock); m.SetTexture("_RockNormal", TerrainTextures.RockNormal); m.SetTexture("_SnowTex", TerrainTextures.Snow); m.SetTexture("_NoiseTex", s.CoreTex("noise_fbm")); return m; } // KeywordEnum 在材质创建时会按属性默认值打开第一个关键字(_UVMODE_MESHUV)。 // 代码里切换时要“关掉全部、再打开一个”,并同步 float 属性,否则 Inspector 与实际变体不一致。 static readonly string[] s_UVModes = { "_UVMODE_MESHUV", "_UVMODE_WORLDXZ", "_UVMODE_TRIPLANAR", "_UVMODE_WEIGHTS" }; static void SetUVMode(Material m, int mode) { foreach (var k in s_UVModes) m.DisableKeyword(k); m.EnableKeyword(s_UVModes[mode]); m.SetFloat("_UVMode", mode); } // ---------------------------------------------------------------- 20.1 [BookShot("ch20-uv-stretch", W = 640, H = 420, Panels = 3)] static void UVStretch(Shot s) { var m = s.Mat("Cookbook/Ch20/UVCompare"); m.SetTexture("_BaseMap", s.CoreTex("uv_grid")); m.SetTextureScale("_BaseMap", new Vector2(4, 4)); m.SetFloat("_Tiling", 0.5f); m.SetFloat("_Sharpness", 6); SetUVMode(m, s.Panel); Terrain(s, m, 14, 160, MesaHeight); s.SunDir(42, -50, 1.4f); s.LookAt(new Vector3(-1.5f, 4.6f, -6.8f), new Vector3(0, 1.0f, 1.2f), 48); } // ---------------------------------------------------------------- 20.2 [BookShot("ch20-triplanar-weights", W = 640, H = 420, Panels = 3)] static void TriplanarWeights(Shot s) { var m = s.Mat("Cookbook/Ch20/UVCompare"); SetUVMode(m, 3); m.SetFloat("_Sharpness", new[] { 1f, 4f, 16f }[s.Panel]); s.MeshObj(Heightfield.Rock(1f, 128, 0.2f, 3f), m, new Vector3(0, 0.9f, 0), 1, "Rock"); s.Floor(s.Lit(new Color(0.5f, 0.5f, 0.5f), 0, 0.1f)); s.SunDir(40, -40, 1.0f); s.LookAt(new Vector3(1.6f, 2.4f, -3.6f), new Vector3(0, 0.8f, 0), 38); } [BookShot("ch20-triplanar-rock", W = 960, H = 460)] static void TriplanarRock(Shot s) { var m = s.Mat("Cookbook/Ch20/Triplanar"); m.SetTexture("_BaseMap", TerrainTextures.Rock); m.SetTexture("_BumpMap", TerrainTextures.RockNormal); m.SetFloat("_Tiling", 0.35f); m.SetFloat("_Sharpness", 8); m.SetFloat("_Smoothness", 0.25f); var rock = s.MeshObj(Heightfield.Rock(1.4f, 160, 0.28f, 7f), m, new Vector3(-1.3f, 1.0f, 0.6f), 1, "Rock"); rock.transform.rotation = Quaternion.Euler(0, 30, 0); var rock2 = s.MeshObj(Heightfield.Rock(0.8f, 128, 0.3f, 2f), m, new Vector3(1.6f, 0.55f, -0.2f), 1, "Rock2"); rock2.transform.rotation = Quaternion.Euler(10, 70, 20); var cube = s.Cube(m, new Vector3(3.6f, 0.6f, 1.2f), 1.2f); cube.transform.rotation = Quaternion.Euler(0, 25, 0); var g = AutoTerrainMat(s); g.SetFloat("_RockSlope", 0.9f); g.SetFloat("_SandHeight", -10); Terrain(s, g, 30, 120, (x, z) => (Heightfield.Fbm(x * 0.1f, z * 0.1f, 3) - 0.5f) * 0.4f); s.SunDir(35, -55, 1.5f); s.LookAt(new Vector3(0.6f, 2.6f, -6.4f), new Vector3(0.8f, 0.7f, 0.4f), 42); } // ---------------------------------------------------------------- 20.3 [BookShot("ch20-triplanar-normal", W = 480, H = 420, Panels = 4)] static void TriplanarNormal(Shot s) { var m = s.Mat("Cookbook/Ch20/Triplanar"); m.SetTexture("_BaseMap", Texture2D.whiteTexture); // 只看法线带来的明暗,颜色用纯灰 m.SetTexture("_BumpMap", s.CoreTex("bricks_normal")); m.SetColor("_BaseColor", new Color(0.7f, 0.7f, 0.7f)); m.SetFloat("_Smoothness", 0.2f); m.SetFloat("_Tiling", 0.9f); m.SetFloat("_Sharpness", 4); m.SetFloat("_BumpScale", 1.2f); m.SetFloat("_NormalMode", s.Panel); s.Sphere(m, new Vector3(0, 1.0f, 0), 1.9f); s.Floor(s.Lit(new Color(0.45f, 0.45f, 0.45f), 0, 0.1f)); s.SunDir(20, -75, 2.2f); s.Background(new Color(0.16f, 0.18f, 0.2f)); RenderSettings.ambientProbe = RenderSettings.ambientProbe * 0.35f; // 压低环境光,让法线细节更明显 s.LookAt(new Vector3(0, 1.6f, -4.2f), new Vector3(0, 0.95f, 0), 40); } // ---------------------------------------------------------------- 20.4 [BookShot("ch20-auto-steps", W = 640, H = 420, Panels = 3)] static void AutoSteps(Shot s) { var m = AutoTerrainMat(s); if (s.Panel == 0) { m.SetFloat("_NoiseAmp", 0); m.SetFloat("_BlendDepth", 1f); m.SetFloat("_SlopeBand", 0.02f); } if (s.Panel == 1) { m.SetFloat("_NoiseAmp", 1.5f); m.SetFloat("_BlendDepth", 1f); } if (s.Panel == 2) { m.SetFloat("_NoiseAmp", 1.5f); m.SetFloat("_BlendDepth", 0.12f); } m.SetFloat("_SnowHeight", 2.3f); m.SetFloat("_SandHeight", -0.05f); m.SetFloat("_HeightBand", s.Panel == 0 ? 0.03f : 0.25f); Terrain(s, m, 14, 200, MesaHeight); s.SunDir(40, -50, 1.4f); s.LookAt(new Vector3(-1.6f, 3.4f, -4.6f), new Vector3(-0.4f, 1.0f, 0.6f), 50); } [BookShot("ch20-auto-terrain", W = 640, H = 400, Panels = 2)] static void AutoTerrain(Shot s) { var m = AutoTerrainMat(s); if (s.Panel == 0) m.EnableKeyword("_DEBUG_WEIGHTS"); Terrain(s, m, 80, 320, Heightfield.DemoHeight); s.SunDir(32, -40, 1.5f); s.LookAt(new Vector3(-4f, 6.5f, -20f), new Vector3(2f, 3.5f, 10f), 50); } // ---------------------------------------------------------------- 20.5 [BookShot("ch20-heightblend", W = 640, H = 420, Panels = 3)] static void HeightBlendShot(Shot s) { var m = s.Mat("Cookbook/Ch20/HeightBlend"); m.SetTexture("_MapA", s.CoreTex("bricks_albedo")); m.SetTextureScale("_MapA", new Vector2(2, 2)); m.SetTexture("_HeightA", s.CoreTex("bricks_height")); m.SetTexture("_NormalA", s.CoreTex("bricks_normal")); m.SetTexture("_MapB", TerrainTextures.Grass); m.SetTexture("_NoiseTex", s.CoreTex("noise_fbm")); m.SetFloat("_TilingB", 3); m.SetFloat("_MaskContrast", 2.2f); m.SetFloat("_UseHeight", s.Panel == 0 ? 0 : 1); m.SetFloat("_BlendDepth", s.Panel == 1 ? 0.25f : 0.05f); s.Grid(m, Vector3.zero, 4f, 8); s.SunDir(55, -30, 1.4f); s.Background(new Color(0.16f, 0.18f, 0.2f)); s.LookAt(new Vector3(0, 3.3f, -2.1f), new Vector3(0, 0, 0.1f), 50); } // ---------------------------------------------------------------- 20.6 /// 程序化“画”一张 Control 图:R 草、G 土路、B 岩石(按坡度)、A 雪(按高度) static Texture2D BuildControl(float size, System.Func<float, float, float> h, int res = 256) { var t = new Texture2D(res, res, TextureFormat.RGBA32, false, true) { wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Bilinear, hideFlags = HideFlags.DontSave }; var px = new Color[res * res]; for (int y = 0; y < res; y++) for (int x = 0; x < res; x++) { float wx = (x / (res - 1f) - 0.5f) * size, wz = (y / (res - 1f) - 0.5f) * size; float e = 0.3f; float dx = (h(wx + e, wz) - h(wx - e, wz)) / (2 * e), dz = (h(wx, wz + e) - h(wx, wz - e)) / (2 * e); float ny = 1f / Mathf.Sqrt(1 + dx * dx + dz * dz); float rock = Smooth(0.82f, 0.74f, ny); float snow = Smooth(6.5f, 8f, h(wx, wz)) * (1 - rock * 0.7f); // 一条蜿蜒的小路:到正弦曲线的距离 float pathX = Mathf.Sin(wz * 0.18f) * 4f - 2f + Heightfield.Fbm(wz * 0.2f, 3f, 2) * 2f; float path = Smooth(1.4f, 0.6f, Mathf.Abs(wx - pathX)) * Smooth(14f, 8f, wz); float grass = 1; var c = new Color(grass, 0, 0, 0); c = Color.Lerp(c, new Color(0, 1, 0, 0), path); c = Color.Lerp(c, new Color(0, 0, 1, 0), rock); c = Color.Lerp(c, new Color(0, 0, 0, 1), snow); px[y * res + x] = c; } t.SetPixels(px); t.Apply(); return t; } static Material SplatMat(Shot s, float size, System.Func<float, float, float> h) { var m = s.Mat("Cookbook/Ch20/Splatmap"); var control = BuildControl(size, h); s.Temp.Add(control); m.SetTexture("_Control", control); m.SetTexture("_Splat0", TerrainTextures.Grass); m.SetTexture("_Splat1", TerrainTextures.Sand); m.SetTexture("_Splat2", TerrainTextures.Rock); m.SetTexture("_Normal2", TerrainTextures.RockNormal); m.SetTexture("_Splat3", TerrainTextures.Snow); return m; } [BookShot("ch20-splatmap", W = 640, H = 400, Panels = 2)] static void Splat(Shot s) { var m = SplatMat(s, 80, Heightfield.DemoHeight); if (s.Panel == 0) m.EnableKeyword("_DEBUG_CONTROL"); Terrain(s, m, 80, 320, Heightfield.DemoHeight); s.SunDir(32, -40, 1.5f); s.LookAt(new Vector3(-4f, 6.5f, -20f), new Vector3(2f, 3.5f, 10f), 50); } [BookShot("ch20-splat-height", W = 640, H = 420, Panels = 2)] static void SplatHeight(Shot s) { var m = SplatMat(s, 80, Heightfield.DemoHeight); m.SetFloat("_HeightBased", s.Panel); m.SetFloat("_HeightTransition", 0.15f); Terrain(s, m, 80, 320, Heightfield.DemoHeight); s.SunDir(40, -40, 1.5f); // 对准土路与草地的交界 float x = -2.6f, z = -6f; float y = Heightfield.DemoHeight(x, z); s.LookAt(new Vector3(x - 0.4f, y + 2.6f, z - 2.2f), new Vector3(x, y, z + 0.3f), 45); } // ---------------------------------------------------------------- 20.7 – 20.9 static GrassRenderer Grass(Shot s, System.Func<float, float, float> h, Rect area, float spacing, System.Func<float, float, bool> accept = null, int seed = 5) { var mesh = GrassRenderer.BuildClump(9, 0.22f, seed); s.Temp.Add(mesh); var mat = s.Mat("Cookbook/Ch20/Grass"); mat.enableInstancing = true; // Graphics.RenderMeshInstanced 要求材质开启 GPU Instancing var rnd = new System.Random(seed); var mats = new List<Matrix4x4>(); var vars = new List<float>(); for (float z = area.yMin; z < area.yMax; z += spacing) for (float x = area.xMin; x < area.xMax; x += spacing) { float px = x + ((float)rnd.NextDouble() - 0.5f) * spacing; float pz = z + ((float)rnd.NextDouble() - 0.5f) * spacing; if (accept != null && !accept(px, pz)) continue; float py = h(px, pz); float sc = Mathf.Lerp(0.75f, 1.35f, (float)rnd.NextDouble()) * Mathf.Lerp(0.8f, 1.2f, Heightfield.Fbm(px * 0.3f, pz * 0.3f, 2)); mats.Add(Matrix4x4.TRS(new Vector3(px, py, pz), Quaternion.Euler(0, (float)rnd.NextDouble() * 360, 0), Vector3.one * sc)); vars.Add((float)rnd.NextDouble()); } var go = new GameObject("Grass"); var gr = go.AddComponent<GrassRenderer>(); gr.mesh = mesh; gr.material = mat; gr.SetInstances(mats, vars); return gr; } static float Meadow(float x, float z) => (Heightfield.Fbm(x * 0.08f + 2, z * 0.08f + 5, 4) - 0.5f) * 1.6f; static Material MeadowGround(Shot s) { var g = AutoTerrainMat(s); g.SetFloat("_RockSlope", 0.9f); g.SetFloat("_SandHeight", -10f); return g; } [BookShot("ch20-grass-field", W = 960, H = 500)] static void GrassField(Shot s) { Terrain(s, MeadowGround(s), 60, 200, Meadow); var gr = Grass(s, Meadow, new Rect(-14, -8, 28, 34), 0.24f); gr.material.SetFloat("_FadeStart", 22); gr.material.SetFloat("_FadeEnd", 30); gr.material.SetFloat("_WindPhase", 1.2f); var rock = s.Mat("Cookbook/Ch20/Triplanar"); rock.SetTexture("_BaseMap", TerrainTextures.Rock); rock.SetTexture("_BumpMap", TerrainTextures.RockNormal); rock.SetFloat("_Tiling", 0.4f); s.MeshObj(Heightfield.Rock(1.1f, 128, 0.3f, 4f), rock, new Vector3(2.4f, Meadow(2.4f, 2f) + 0.2f, 2f), 1, "Rock"); s.SunDir(22, -150, 1.6f); // 逆光,看透射 s.LookAt(new Vector3(0, Meadow(0, -6) + 1.5f, -6), new Vector3(1.0f, Meadow(1, 4) + 0.3f, 4), 50); } [BookShot("ch20-grass-interact", W = 640, H = 420, Panels = 2)] static void GrassInteract(Shot s) { System.Func<float, float, float> flat = (x, z) => (Heightfield.Fbm(x * 0.1f, z * 0.1f, 3) - 0.5f) * 0.3f; Terrain(s, MeadowGround(s), 30, 100, flat); var gr = Grass(s, flat, new Rect(-6, -4, 12, 12), 0.16f); var ballMat = s.Lit(new Color(0.85f, 0.3f, 0.15f), 0, 0.6f); var b1 = s.Sphere(ballMat, new Vector3(-0.9f, flat(-0.9f, 0.8f) + 0.45f, 0.8f), 0.9f); var b2 = s.Sphere(s.Lit(new Color(0.2f, 0.45f, 0.85f), 0, 0.6f), new Vector3(1.2f, flat(1.2f, 2.2f) + 0.35f, 2.2f), 0.7f); if (s.Panel == 1) { // 两个球 + 红球身后一串看不见的点:模拟球滚过后留下的“压痕带” var list = new List<Transform> { b1.transform, b2.transform }; for (int i = 1; i <= 5; i++) { var t = new GameObject("Trail" + i).transform; t.position = b1.transform.position + new Vector3(-0.35f * i, 0, -0.5f * i); t.localScale = Vector3.one * (0.9f - i * 0.08f); list.Add(t); } gr.interactors = list.ToArray(); gr.interactorRadius = 1.1f; // 半径 = 1.1 × 物体缩放 } s.SunDir(45, -40, 1.4f); s.LookAt(new Vector3(0.6f, 3.0f, -3.4f), new Vector3(-0.3f, 0.0f, 0.9f), 50); } [BookShot("ch20-grass-fade", W = 640, H = 400, Panels = 2)] static void GrassFade(Shot s) { Terrain(s, MeadowGround(s), 80, 200, Meadow); var gr = Grass(s, Meadow, new Rect(-16, -6, 32, 40), 0.3f); if (s.Panel == 0) { gr.material.SetFloat("_FadeStart", 14f); gr.material.SetFloat("_FadeEnd", 14.01f); } else { gr.material.SetFloat("_FadeStart", 9f); gr.material.SetFloat("_FadeEnd", 15f); } s.SunDir(40, -40, 1.4f); s.LookAt(new Vector3(0, Meadow(0, -6) + 3.0f, -6), new Vector3(0, Meadow(0, 10), 10), 55); } [BookShot("ch20-final", W = 960, H = 520)] static void Final(Shot s) { var m = AutoTerrainMat(s); Terrain(s, m, 80, 320, Heightfield.DemoHeight); // 只在平缓、低于雪线的地方长草 System.Func<float, float, bool> accept = (x, z) => { float e = 0.3f, h = Heightfield.DemoHeight(x, z); float dx = (Heightfield.DemoHeight(x + e, z) - Heightfield.DemoHeight(x - e, z)) / (2 * e); float dz = (Heightfield.DemoHeight(x, z + e) - Heightfield.DemoHeight(x, z - e)) / (2 * e); return dx * dx + dz * dz < 0.12f && h < 4f && h > -0.3f; }; var gr = Grass(s, Heightfield.DemoHeight, new Rect(-16, -14, 30, 30), 0.26f, accept); gr.material.SetFloat("_FadeStart", 20); gr.material.SetFloat("_FadeEnd", 28); gr.material.SetFloat("_WindPhase", 2f); RenderSettings.fog = true; RenderSettings.fogMode = FogMode.Exponential; RenderSettings.fogDensity = 0.012f; RenderSettings.fogColor = new Color(0.55f, 0.62f, 0.72f); s.SunDir(28, -35, 1.6f); s.LookAt(new Vector3(-6f, Heightfield.DemoHeight(-6, -16) + 2.2f, -16f), new Vector3(2f, 3.2f, 12f), 55); } } }