源码 / Ch15/Editor/Shots15.cs
Ch15/Editor/Shots15.cs
// 第 15 章配图 using Cookbook.Core; using UnityEngine; using UnityEngine.Rendering.Universal; namespace Cookbook.Ch15 { static class Shots15 { static readonly Color Paper = new Color(0.96f, 0.94f, 0.88f); // 三件套静物:纽结、球、方块 static void StillLife(Shot s, Material knot, Material sphere, Material cube) { s.Knot(knot, new Vector3(-0.95f, 0.95f, 0.35f), 1.0f); s.Sphere(sphere, new Vector3(1.15f, 0.55f, 0.1f), 1.1f); var c = s.Cube(cube, new Vector3(0.15f, 0.35f, -0.95f), 0.7f); c.transform.rotation = Quaternion.Euler(0, 35, 0); s.LookAt(new Vector3(0, 2.0f, -4.8f), new Vector3(0, 0.55f, 0), 38); } static Material Unlit(Shot s, Texture tex) { var m = s.Mat("Universal Render Pipeline/Unlit"); m.SetTexture("_BaseMap", tex); return m; } // ---------------- Gooch ---------------- [BookShot("ch15-gooch", W = 640, H = 480, Panels = 3)] static void Gooch(Shot s) { s.Background(new Color(0.8f, 0.82f, 0.85f)); s.Floor(s.Lit(new Color(0.78f, 0.78f, 0.78f), 0, 0.1f)); Color[] cols = { new Color(0.8f, 0.25f, 0.2f), new Color(0.25f, 0.5f, 0.8f), new Color(0.9f, 0.75f, 0.3f) }; Material M(int i) { if (s.Panel == 0) return s.Lit(cols[i], 0, 0.6f); var m = s.Mat("Cookbook/Ch15/Gooch"); m.SetColor("_BaseColor", cols[i]); if (s.Panel == 1) { m.SetFloat("_OutlineWidth", 0); m.SetFloat("_SpecStrength", 0); } return m; } StillLife(s, M(0), M(1), M(2)); s.SunDir(42, -35, 1.3f); } // ---------------- Hatching ---------------- static Texture2D s_H0, s_H1, s_HPreview; static void EnsureHatch() { if (s_H0 != null) return; var lv = HatchTextures.BuildLevels(256); HatchTextures.Pack(lv, 256, out s_H0, out s_H1); s_HPreview = HatchTextures.Preview(lv, 256); s_H0.hideFlags = s_H1.hideFlags = s_HPreview.hideFlags = HideFlags.DontSave; } [BookShot("ch15-tam", W = 960, H = 152)] static void Tam(Shot s) { EnsureHatch(); s.Background(Color.white); s.Cam.orthographic = true; s.Cam.orthographicSize = 0.5f; s.Cam.transform.position = new Vector3(0, 0, -5); s.Cam.transform.rotation = Quaternion.identity; var q = s.Quad(Unlit(s, s_HPreview), Vector3.zero); q.transform.localScale = new Vector3((float)s_HPreview.width / s_HPreview.height, 1, 1); } [BookShot("ch15-hatching", W = 640, H = 480, Panels = 3)] static void Hatching(Shot s) { EnsureHatch(); s.Background(Paper); Material H(float scale, Color tint) { var m = s.Mat("Cookbook/Ch15/Hatching"); m.SetTexture("_Hatch0", s_H0); m.SetTexture("_Hatch1", s_H1); m.SetFloat("_HatchScale", scale); m.SetFloat("_ScreenSpace", s.Panel == 1 ? 1 : 0); if (s.Panel == 2) { m.SetColor("_BaseColor", tint); m.SetFloat("_Tint", 0.75f); m.SetColor("_InkColor", new Color(0.12f, 0.1f, 0.3f)); } return m; } var floor = H(30, new Color(0.9f, 0.85f, 0.7f)); floor.SetFloat("_OutlineWidth", 0); floor.SetFloat("_Exposure", 1.3f); s.Floor(floor); StillLife(s, H(1.2f, new Color(0.95f, 0.45f, 0.35f)), H(3f, new Color(0.45f, 0.65f, 0.95f)), H(1.5f, new Color(0.95f, 0.85f, 0.4f))); s.SunDir(40, -40, 1.3f); } // ---------------- Halftone ---------------- [BookShot("ch15-halftone", W = 640, H = 480, Panels = 3)] static void Halftone(Shot s) { s.Background(Paper); string[] kw = { "_STYLE_OFF", "_STYLE_MONO", "_STYLE_CMYK" }; Material M(Color c) { var m = s.Mat("Cookbook/Ch15/Halftone"); m.SetColor("_BaseColor", c); m.SetFloat("_Style", s.Panel); foreach (var k in kw) m.DisableKeyword(k); m.EnableKeyword(kw[s.Panel]); m.SetFloat("_CellSize", 7); return m; } s.Floor(M(new Color(0.75f, 0.85f, 0.7f))); StillLife(s, M(new Color(0.85f, 0.25f, 0.25f)), M(new Color(0.2f, 0.5f, 0.85f)), M(new Color(0.95f, 0.8f, 0.25f))); s.SunDir(40, -40, 1.2f); } // ---------------- Watercolor ---------------- [BookShot("ch15-watercolor", W = 640, H = 480, Panels = 2)] static void Watercolor(Shot s) { s.Background(Paper); Material M(Color c, float density = 1.1f) { if (s.Panel == 0) return s.Lit(c, 0, 0.3f); var m = s.Mat("Cookbook/Ch15/Watercolor"); m.SetColor("_BaseColor", c); m.SetTexture("_NoiseTex", s.CoreTex("noise_fbm")); m.SetFloat("_Density", density); return m; } s.Floor(M(new Color(0.85f, 0.8f, 0.68f), 0.55f)); StillLife(s, M(new Color(0.3f, 0.6f, 0.65f)), M(new Color(0.9f, 0.35f, 0.3f)), M(new Color(0.95f, 0.75f, 0.25f))); s.SunDir(40, -40, 1.3f); } // ---------------- Retro / PS1 ---------------- static Texture2D PixelTex(int size, System.Func<int, int, Color> f, string name) { var t = new Texture2D(size, size, TextureFormat.RGBA32, false, false) { name = name, filterMode = FilterMode.Bilinear, wrapMode = TextureWrapMode.Repeat, hideFlags = HideFlags.DontSave }; var px = new Color[size * size]; for (int y = 0; y < size; y++) for (int x = 0; x < size; x++) px[y * size + x] = f(x, y); t.SetPixels(px); t.Apply(); return t; } static float Hash(int x, int y) { uint h = (uint)(x * 374761393 + y * 668265263); h = (h ^ (h >> 13)) * 1274126177u; return (h & 0xffff) / 65535f; } static Texture2D StoneTiles() => PixelTex(32, (x, y) => { bool mortar = x % 16 == 0 || y % 16 == 0; int tile = (x / 16) + (y / 16) * 2; float n = Hash(x, y) * 0.15f + Hash(tile, 7) * 0.1f; return mortar ? new Color(0.25f, 0.22f, 0.2f) : new Color(0.55f + n, 0.5f + n, 0.42f + n * 0.8f); }, "StoneTiles"); static Texture2D Crate() => PixelTex(32, (x, y) => { bool border = x < 3 || y < 3 || x > 28 || y > 28; bool diag = Mathf.Abs(x - y) < 2 || Mathf.Abs(x - (31 - y)) < 2; bool plank = y % 8 == 0; float n = Hash(x, y) * 0.08f; if (border || diag) return new Color(0.42f + n, 0.26f + n, 0.12f); return plank ? new Color(0.45f, 0.3f, 0.15f) : new Color(0.7f + n, 0.5f + n, 0.26f); }, "Crate"); [BookShot("ch15-retro", W = 640, H = 480, Panels = 3)] static void Retro(Shot s) { var stone = StoneTiles(); var crate = Crate(); Material M(Texture2D tex, Vector2 tiling) { var m = s.Mat("Cookbook/Ch15/RetroPS1"); m.SetTexture("_BaseMap", tex); m.SetTextureScale("_BaseMap", tiling); m.SetVector("_VirtualRes", new Vector4(128, 96, 0, 0)); m.SetFloat("_VertexSnap", s.Panel == 2 ? 1 : 0); m.SetFloat("_Affine", s.Panel >= 1 ? 1 : 0); m.SetFloat("_PointSample", s.Panel >= 1 ? 1 : 0); m.SetFloat("_Quantize", s.Panel == 2 ? 1 : 0); m.SetFloat("_ColorBits", 3); return m; } s.Background(new Color(0.18f, 0.16f, 0.22f)); s.Grid(M(stone, new Vector2(8, 8)), new Vector3(0, 0, 3), 16, 2); // 只有 8 个三角形的大地板 var wall = s.Grid(M(stone, new Vector2(8, 2)), new Vector3(0, 2, 7), 16, 2); wall.transform.rotation = Quaternion.Euler(-90, 0, 0); wall.transform.localScale = new Vector3(1, 1, 0.25f); var c1 = s.Cube(M(crate, Vector2.one), new Vector3(-1.1f, 0.5f, 0.6f), 1f); c1.transform.rotation = Quaternion.Euler(0, 25, 0); var c2 = s.Cube(M(crate, Vector2.one), new Vector3(0.9f, 0.4f, 1.8f), 0.8f); c2.transform.rotation = Quaternion.Euler(0, -15, 0); s.Cube(M(crate, Vector2.one), new Vector3(-0.9f, 1.25f, 0.7f), 0.5f).transform.rotation = Quaternion.Euler(0, 50, 0); s.LookAt(new Vector3(0.4f, 1.1f, -2.6f), new Vector3(0, 0.6f, 2f), 60); s.SunDir(35, -50, 1.2f); } // ---------------- Matcap ---------------- static Texture2D s_McClay, s_McToon, s_McChrome; static void EnsureMatcaps() { if (s_McClay != null) return; s_McClay = MatcapGenerator.Build(256, MatcapGenerator.Clay, "Clay"); s_McToon = MatcapGenerator.Build(256, MatcapGenerator.Toon, "Toon"); s_McChrome = MatcapGenerator.Build(256, MatcapGenerator.Chrome, "Chrome"); s_McClay.hideFlags = s_McToon.hideFlags = s_McChrome.hideFlags = HideFlags.DontSave; } // 在画面左上角放一张“贴图缩略图” static void Inset(Shot s, Texture2D tex, float size = 0.55f) { var t = s.Cam.transform; var mat = Unlit(s, DiscOnly(tex)); mat.SetFloat("_AlphaClip", 1); mat.SetFloat("_Cutoff", 0.5f); mat.EnableKeyword("_ALPHATEST_ON"); var q = s.Quad(mat, Vector3.zero, size); float dist = 3f; float h = Mathf.Tan(s.Cam.fieldOfView * 0.5f * Mathf.Deg2Rad) * dist; float w = h * 640f / 480f; q.transform.position = t.position + t.forward * dist - t.right * (w - size * 0.62f) + t.up * (h - size * 0.62f); q.transform.rotation = t.rotation; } // 把圆盘外的像素设为透明(配合 Alpha Clip),缩略图看起来就是一个“球” static Texture2D DiscOnly(Texture2D src) { int n = src.width; var px = src.GetPixels(); for (int y = 0; y < n; y++) for (int x = 0; x < n; x++) { float u = (x + 0.5f) / n * 2 - 1, v = (y + 0.5f) / n * 2 - 1; if (u * u + v * v > 0.98f) px[y * n + x] = new Color(0, 0, 0, 0); } var t = new Texture2D(n, n, TextureFormat.RGBA32, false) { hideFlags = HideFlags.DontSave }; t.SetPixels(px); t.Apply(); return t; } [BookShot("ch15-matcap", W = 640, H = 480, Panels = 3)] static void Matcap(Shot s) { EnsureMatcaps(); var tex = new[] { s_McClay, s_McToon, s_McChrome }[s.Panel]; var m = s.Mat("Cookbook/Ch15/MatcapStylized"); m.SetTexture("_MatCap", tex); s.Background(new Color(0.16f, 0.17f, 0.19f)); s.Floor(s.Lit(new Color(0.3f, 0.3f, 0.32f), 0, 0.1f)); s.Knot(m, new Vector3(-0.3f, 1.0f, 0.3f), 1.05f); s.Sphere(m, new Vector3(1.3f, 0.5f, -0.3f), 1.0f); s.Capsule(m, new Vector3(-1.75f, 0.75f, -0.4f), 0.75f); s.LookAt(new Vector3(0, 1.9f, -4.8f), new Vector3(0, 0.6f, 0), 38); s.SunDir(55, -30, 1.2f); Inset(s, tex); } [BookShot("ch15-matcap-fix", W = 640, H = 400, Panels = 2)] static void MatcapFix(Shot s) { EnsureMatcaps(); var m = s.Mat("Cookbook/Ch15/MatcapStylized"); m.SetTexture("_MatCap", s_McChrome); m.SetFloat("_PerspectiveFix", s.Panel); m.SetFloat("_ShadowStrength", 0); s.Background(new Color(0.16f, 0.17f, 0.19f)); for (int j = -1; j <= 1; j++) for (int i = 0; i < 5; i++) s.Sphere(m, new Vector3(-2.6f + i * 1.3f, j * 1.2f, 0), 1.0f); s.LookAt(new Vector3(0, 0, -2.2f), Vector3.zero, 90); } // ---------------- Flat shading ---------------- [BookShot("ch15-flat", W = 640, H = 480, Panels = 2)] static void Flat(Shot s) { var terrain = s.Mat("Cookbook/Ch15/FlatShading"); terrain.SetFloat("_Terrain", 1); terrain.SetFloat("_Flat", s.Panel); terrain.SetTexture("_HeightTex", s.CoreTex("noise_fbm")); terrain.SetFloat("_HeightScale", 1.5f); terrain.SetFloat("_NoiseTiling", 0.05f); s.Grid(terrain, new Vector3(0, 0, 0.5f), 11, 30); var ball = s.Mat("Cookbook/Ch15/FlatShading"); ball.SetFloat("_Flat", s.Panel); s.MeshObj(BarycentricBaker.Icosphere(1), ball, new Vector3(2.2f, 2.6f, 1.5f), 1.3f); s.LookAt(new Vector3(0, 8.5f, -11.5f), new Vector3(0, -0.3f, 0.8f), 40); s.SunDir(38, -55, 1.1f); } // ---------------- Wireframe ---------------- [BookShot("ch15-wireframe", W = 640, H = 480, Panels = 3, Hdr = true)] static void Wireframe(Shot s) { Mesh B(PrimitiveType type, bool hide) { var go = GameObject.CreatePrimitive(type); var mesh = BarycentricBaker.Bake(go.GetComponent<MeshFilter>().sharedMesh, hide); Object.DestroyImmediate(go); return mesh; } if (s.Panel < 2) { bool hide = s.Panel == 1; var m = s.Mat("Cookbook/Ch15/BaryWireframe"); s.Background(new Color(0.85f, 0.87f, 0.9f)); s.Floor(s.Lit(new Color(0.7f, 0.7f, 0.72f), 0, 0.1f)); s.MeshObj(B(PrimitiveType.Sphere, hide), m, new Vector3(-1.2f, 0.65f, 0.2f), 1.3f); s.MeshObj(B(PrimitiveType.Cube, hide), m, new Vector3(0.35f, 0.45f, -0.6f), 0.9f).transform.rotation = Quaternion.Euler(0, 30, 0); if (hide) s.MeshObj(BarycentricBaker.Bake(Meshes.Grid(1.4f, 6), true), m, new Vector3(1.45f, 0.75f, 0.6f)).transform.rotation = Quaternion.Euler(-70, -20, 0); else s.MeshObj(BarycentricBaker.Bake(BarycentricBaker.Icosphere(1), false), m, new Vector3(1.4f, 0.65f, 0.5f), 1.3f); s.LookAt(new Vector3(0, 2.0f, -4.4f), new Vector3(0, 0.55f, 0), 38); s.SunDir(45, -35, 1.2f); } else { var m = s.Mat("Cookbook/Ch15/HoloWireframe"); m.SetFloat("_Phase", 0.3f); s.Background(new Color(0.01f, 0.015f, 0.03f)); s.MeshObj(BarycentricBaker.Bake(BarycentricBaker.Icosphere(2), false), m, new Vector3(-1.2f, 0.75f, 0.2f), 1.4f); s.MeshObj(B(PrimitiveType.Capsule, true), m, new Vector3(0.25f, 0.9f, -0.4f), 0.9f); s.MeshObj(BarycentricBaker.Bake(Meshes.Grid(8, 16), true), m, new Vector3(0, 0, 1)); s.MeshObj(B(PrimitiveType.Cube, true), m, new Vector3(1.45f, 0.5f, 0.4f), 0.9f).transform.rotation = Quaternion.Euler(0, 30, 0); s.LookAt(new Vector3(0, 2.0f, -4.4f), new Vector3(0, 0.55f, 0), 38); var p = s.Post(); var b = p.Add<Bloom>(); b.intensity.Override(1.2f); b.threshold.Override(0.8f); b.scatter.Override(0.6f); } } } }