源码 / Ch12/Editor/Ch12Shots.cs
Ch12/Editor/Ch12Shots.cs
// 第 12 章配图 using Cookbook.Core; using UnityEngine; namespace Cookbook.Ch12 { static class Shots { // ---------- 小工具 ---------- static void Kw(Material m, string prefix, string[] opts, int pick) { for (int i = 0; i < opts.Length; i++) if (i == pick) m.EnableKeyword(prefix + opts[i]); else m.DisableKeyword(prefix + opts[i]); } // 深色哑光地面 + 灰蓝背景:材质展示用的“摄影棚” static void Studio(Shot s, float floorGray = 0.18f) { var f = s.Lit(new Color(floorGray, floorGray, floorGray), 0, 0.25f); s.Floor(f); s.Background(new Color(0.16f, 0.18f, 0.21f)); } // 程序生成“发丝噪声”:沿 V 方向几乎不变,沿 U 方向逐列随机 static Texture2D StrandNoise(Shot s) { const int W = 512, H = 64; var t = new Texture2D(W, H, TextureFormat.RGBA32, true, true) { wrapMode = TextureWrapMode.Repeat, name = "StrandNoise" }; var rnd = new System.Random(3); // 两层一维值噪声:低频 = 一绺一绺的发束,高频 = 单根发丝 var lo = new float[33]; var hi = new float[W]; for (int k = 0; k < lo.Length; k++) lo[k] = (float)rnd.NextDouble(); lo[32] = lo[0]; for (int x = 0; x < W; x++) hi[x] = (float)rnd.NextDouble(); var px = new Color[W * H]; for (int y = 0; y < H; y++) for (int x = 0; x < W; x++) { float f = x / (float)W * 32f; int i0 = (int)f; float w = f - i0; w = w * w * (3 - 2 * w); float v = 0.7f * Mathf.Lerp(lo[i0], lo[i0 + 1], w) + 0.3f * hi[x]; px[y * W + x] = new Color(v, v, v, 1); } t.SetPixels(px); t.Apply(); s.Temp.Add(t); return t; } // 圆盘网格:UV 与 XZ 平面坐标对应(中心 0.5,0.5),给径向拉丝用 static Mesh Disc(float radius, int rings, int segs) { var v = new System.Collections.Generic.List<Vector3>(); var uv = new System.Collections.Generic.List<Vector2>(); var idx = new System.Collections.Generic.List<int>(); v.Add(Vector3.zero); uv.Add(new Vector2(0.5f, 0.5f)); for (int r = 1; r <= rings; r++) for (int k = 0; k < segs; k++) { float a = k * Mathf.PI * 2 / segs, rr = radius * r / rings; var p = new Vector3(Mathf.Cos(a) * rr, 0, Mathf.Sin(a) * rr); v.Add(p); uv.Add(new Vector2(p.x / radius * 0.5f + 0.5f, p.z / radius * 0.5f + 0.5f)); } for (int k = 0; k < segs; k++) { idx.Add(0); idx.Add(1 + (k + 1) % segs); idx.Add(1 + k); } for (int r = 1; r < rings; r++) for (int k = 0; k < segs; k++) { int a = 1 + (r - 1) * segs + k, b = 1 + (r - 1) * segs + (k + 1) % segs; int c = a + segs, d = b + segs; idx.Add(a); idx.Add(b); idx.Add(c); idx.Add(b); idx.Add(d); idx.Add(c); } var m = new Mesh { indexFormat = UnityEngine.Rendering.IndexFormat.UInt32 }; m.SetVertices(v); m.SetUVs(0, uv); m.SetTriangles(idx, 0); m.RecalculateNormals(); m.RecalculateTangents(); m.RecalculateBounds(); return m; } // 两排球:上排金属(金),下排电介质(红),光滑度从左到右 0.1 → 0.95 static void SphereGrid(Shot s, System.Func<Color, float, float, Material> make) { Color gold = new Color(1.0f, 0.78f, 0.34f), red = new Color(0.7f, 0.08f, 0.06f); for (int row = 0; row < 2; row++) s.SphereRow(5, i => { float sm = Mathf.Lerp(0.1f, 0.95f, i / 4f); return row == 0 ? make(gold, 1, sm) : make(red, 0, sm); }, y: row == 0 ? 1.55f : 0.5f, step: 1.05f, scale: 0.95f); } // ---------- 12.2 基准 ---------- [BookShot("ch12-base-check", W = 640, H = 420, Panels = 2)] static void BaseCheck(Shot s) { s.Floor(); SphereGrid(s, (c, metal, sm) => { if (s.Panel == 0) return s.Lit(c, metal, sm); var m = s.Mat("Cookbook/Ch12/BaseGGX"); m.SetColor("_BaseColor", c); m.SetFloat("_Metallic", metal); m.SetFloat("_Smoothness", sm); return m; }); s.LookAt(new Vector3(0, 1.6f, -7.2f), new Vector3(0, 1.0f, 0), 34); } // ---------- 12.3 清漆 ---------- static Material CarPaint(Shot s, float coat, float energy, float flakes = 0.2f) { var m = s.Mat("Cookbook/Ch12/ClearCoat"); m.SetColor("_BaseColor", new Color(0.55f, 0.03f, 0.03f)); m.SetFloat("_Metallic", 0.6f); m.SetFloat("_Smoothness", 0.45f); m.SetFloat("_FlakeDensity", 300); m.SetFloat("_FlakeStrength", flakes); m.SetFloat("_CoatMask", coat); m.SetFloat("_CoatSmoothness", 0.95f); m.SetFloat("_CoatEnergy", energy); m.SetFloat("_CoatAffectBase", 1); return m; } [BookShot("ch12-clearcoat", W = 640, H = 480, Panels = 3)] static void ClearCoatPanels(Shot s) { Studio(s); var m = s.Panel == 0 ? CarPaint(s, 0, 1) : s.Panel == 1 ? CarPaint(s, 1, 1) : CarPaint(s, 1, 0); s.Knot(m, new Vector3(0, 0.95f, 0), 1.15f); s.SunDir(35, -40, 1.6f); s.Point(new Vector3(-1.8f, 2.2f, -1.5f), new Color(1f, 0.95f, 0.9f), 3, 6); s.LookAt(new Vector3(0, 1.7f, -3.6f), new Vector3(0, 0.9f, 0), 36); } [BookShot("ch12-clearcoat-row", W = 960, H = 330)] static void ClearCoatRow(Shot s) { Studio(s); s.SphereRow(5, i => { var m = CarPaint(s, i / 4f, 1, 0.25f); m.SetFloat("_Smoothness", 0.3f); return m; }, y: 0.5f, step: 1.1f, scale: 0.95f); s.SunDir(40, -35, 1.5f); s.LookAt(new Vector3(0, 1.1f, -5.6f), new Vector3(0, 0.5f, 0), 33); } [BookShot("ch12-clearcoat-urp", W = 640, H = 360, Panels = 2)] static void ClearCoatVsUrp(Shot s) { Studio(s); Color col = new Color(0.08f, 0.2f, 0.55f); for (int k = 0; k < 3; k++) { const float baseSm = 0.4f; float coatSm = new[] { 0.6f, 0.8f, 0.95f }[k]; Material m; if (s.Panel == 0) { m = s.Mat("Universal Render Pipeline/Complex Lit"); m.SetColor("_BaseColor", col); m.SetFloat("_Metallic", 0.3f); m.SetFloat("_Smoothness", baseSm); m.SetFloat("_ClearCoat", 1); m.EnableKeyword("_CLEARCOAT"); m.SetFloat("_ClearCoatMask", 1); m.SetFloat("_ClearCoatSmoothness", coatSm); } else { m = s.Mat("Cookbook/Ch12/ClearCoat"); m.SetColor("_BaseColor", col); m.SetFloat("_Metallic", 0.3f); m.SetFloat("_Smoothness", baseSm); m.SetFloat("_FlakeStrength", 0); m.SetFloat("_CoatMask", 1); m.SetFloat("_CoatSmoothness", coatSm); m.SetFloat("_CoatEnergy", 1); m.SetFloat("_CoatAffectBase", 1); } s.Sphere(m, new Vector3((k - 1) * 1.15f, 0.55f, 0), 1.0f); } s.SunDir(50, -30, 1.5f); // 与天空盒里太阳的方向一致,直接光高光和环境反射里的太阳重合 s.LookAt(new Vector3(0, 1.2f, -5.0f), new Vector3(0, 0.55f, 0), 34); } // 白炉测试(Furnace Test):均匀的 0.5 灰环境,没有直接光。 // 能量守恒的材质应该和背景“融为一体”,多出来的能量会在边缘发亮 [BookShot("ch12-clearcoat-furnace", W = 640, H = 400, Panels = 3)] static void ClearCoatFurnace(Shot s) { const float L = 0.5f; var cube = new Cubemap(16, TextureFormat.RGBAHalf, true) { hideFlags = HideFlags.DontSave }; var px = new Color[16 * 16]; for (int k = 0; k < px.Length; k++) px[k] = new Color(L, L, L, 1); for (int f = 0; f < 6; f++) cube.SetPixels(px, (CubemapFace)f); cube.Apply(true); s.Temp.Add(cube); RenderSettings.customReflectionTexture = cube; var sh = new UnityEngine.Rendering.SphericalHarmonicsL2(); sh.AddAmbientLight(new Color(L, L, L)); RenderSettings.ambientProbe = sh; s.Background(new Color(L, L, L).gamma); // 背景色按 sRGB 解释,要换算成线性 0.5 s.Sun.intensity = 0; var m = s.Mat("Cookbook/Ch12/ClearCoat"); m.SetColor("_BaseColor", Color.white); m.SetFloat("_Metallic", 0); m.SetFloat("_Smoothness", 0.5f); m.SetFloat("_FlakeStrength", 0); m.SetFloat("_CoatMask", s.Panel == 0 ? 0 : 1); m.SetFloat("_CoatSmoothness", 0.9f); m.SetFloat("_CoatEnergy", s.Panel == 2 ? 0 : 1); m.SetFloat("_CoatAffectBase", 1); s.Sphere(m, new Vector3(0, 0, 0), 1.6f); s.LookAt(new Vector3(0, 0, -4.2f), Vector3.zero, 34); } // ---------- 12.4 各向异性 ---------- static Material Brushed(Shot s, float aniso, int dir, float ibl = 1) { var m = s.Mat("Cookbook/Ch12/Anisotropic"); m.SetColor("_BaseColor", new Color(0.93f, 0.92f, 0.9f)); m.SetFloat("_Smoothness", 0.62f); m.SetFloat("_Anisotropy", aniso); m.SetTexture("_BrushTex", s.CoreTex("noise_streak")); m.SetFloat("_BrushStrength", 0.25f); m.SetFloat("_AnisoIBL", ibl); Kw(m, "_DIR_", new[] { "TANGENT", "BITANGENT", "RADIAL" }, dir); return m; } [BookShot("ch12-aniso", W = 640, H = 480, Panels = 3)] static void AnisoPanels(Shot s) { Studio(s); float k = new[] { 0f, 0.5f, 0.85f }[s.Panel]; s.Knot(Brushed(s, k, 0), new Vector3(0, 0.95f, 0), 1.15f); s.SunDir(30, -20, 1.2f); s.Point(new Vector3(-1.2f, 1.8f, -1.6f), new Color(1f, 0.9f, 0.8f), 4, 5); s.Point(new Vector3(1.4f, 1.2f, -1.4f), new Color(0.7f, 0.85f, 1f), 3, 5); s.LookAt(new Vector3(0, 1.7f, -3.6f), new Vector3(0, 0.9f, 0), 36); } [BookShot("ch12-aniso-disc", W = 640, H = 420, Panels = 2)] static void AnisoDisc(Shot s) { Studio(s, 0.1f); var m = Brushed(s, s.Panel == 0 ? 0f : 0.85f, 2); m.SetFloat("_Smoothness", 0.7f); m.SetTextureScale("_BrushTex", new Vector2(1, 1)); var go = s.MeshObj(Disc(1.4f, 48, 160), m, new Vector3(0, 0.02f, 0), 1, "Disc"); // 主光放在相机身后,它的镜面反射不会落进画面;高光全部来自两盏点光 s.SunDir(50, -30, 0.6f); s.Point(new Vector3(-0.7f, 1.0f, 0.6f), new Color(1f, 0.95f, 0.88f), 1.0f, 6); s.Point(new Vector3(0.8f, 0.8f, -0.3f), new Color(0.75f, 0.85f, 1f), 0.5f, 5); s.LookAt(new Vector3(0, 2.6f, -2.4f), new Vector3(0, 0, 0.1f), 42); } [BookShot("ch12-aniso-ibl", W = 640, H = 400, Panels = 2)] static void AnisoIbl(Shot s) { Studio(s); // 球体的切线沿纬线(水平),拉丝水平 → 环境里的地平线应被竖直方向拉开 var m = Brushed(s, 0.85f, 0, s.Panel); m.SetFloat("_Smoothness", 0.85f); m.SetFloat("_BrushStrength", 0.1f); s.Sphere(m, new Vector3(0, 0.85f, 0), 1.5f); s.Sun.intensity = 0.0f; // 只看环境反射 s.LookAt(new Vector3(0, 1.2f, -3.6f), new Vector3(0, 0.85f, 0), 36); } // ---------- 12.5 头发 ---------- static Material Hair(Shot s, int mode, float shiftAmount = 0.3f) { var m = s.Mat("Cookbook/Ch12/HairKK"); Kw(m, "_MODE_", new[] { "BLINNPHONG", "SINGLE", "DUAL" }, mode); m.SetTexture("_ShiftTex", StrandNoise(s)); m.SetFloat("_StrandAlongV", 1); m.SetFloat("_ShiftAmount", shiftAmount); return m; } static void HairScene(Shot s, Material m) { Studio(s); var head = s.Sphere(m, new Vector3(0, 1.0f, 0), 1.6f); head.transform.rotation = Quaternion.Euler(0, 0, 0); s.SunDir(35, -25, 1.5f); s.LookAt(new Vector3(0, 1.45f, -3.6f), new Vector3(0, 0.95f, 0), 34); } [BookShot("ch12-hair", W = 640, H = 480, Panels = 3)] static void HairPanels(Shot s) => HairScene(s, Hair(s, s.Panel)); [BookShot("ch12-hair-shift", W = 640, H = 420, Panels = 2)] static void HairShift(Shot s) => HairScene(s, Hair(s, 2, s.Panel == 0 ? 0f : 0.3f)); // ---------- 12.6 布料 ---------- static Material Velvet(Shot s, int model, float sheenRough = 0.45f) { var m = s.Mat("Cookbook/Ch12/Cloth"); Kw(m, "_MODEL_", new[] { "GGX", "CHARLIE", "SUBSURFACE" }, model); m.SetFloat("_SheenRoughness", sheenRough); m.SetFloat("_WeaveScale", 90); m.SetColor("_BaseColor", new Color(0.22f, 0.015f, 0.04f)); m.SetColor("_SheenColor", new Color(0.95f, 0.55f, 0.6f)); m.SetColor("_SubsurfaceColor", new Color(0.6f, 0.08f, 0.15f)); m.SetFloat("_Wrap", 0.6f); return m; } [BookShot("ch12-cloth", W = 640, H = 480, Panels = 3)] static void ClothPanels(Shot s) { Studio(s, 0.12f); var m = Velvet(s, s.Panel); // 一个“抱枕”:压扁的球 + 一个纽结 var pillow = s.Sphere(m, new Vector3(-0.55f, 0.38f, 0.2f), 1); pillow.transform.localScale = new Vector3(1.5f, 0.75f, 1.2f); s.Knot(m, new Vector3(0.75f, 0.85f, -0.1f), 0.75f); // 逆光为主:布料的绒毛反光主要出现在轮廓和掠射角 s.SunDir(25, 160, 2.2f); s.Point(new Vector3(-1.5f, 1.6f, -2.0f), new Color(1f, 0.95f, 0.9f), 1.2f, 6); s.LookAt(new Vector3(0, 1.6f, -3.4f), new Vector3(0.05f, 0.55f, 0), 38); } [BookShot("ch12-cloth-row", W = 960, H = 330)] static void ClothRow(Shot s) { Studio(s, 0.12f); s.SphereRow(5, i => Velvet(s, 1, Mathf.Lerp(0.15f, 1f, i / 4f)), y: 0.5f, step: 1.1f, scale: 0.95f); s.SunDir(25, 160, 2.2f); s.Point(new Vector3(0, 1.6f, -2.5f), new Color(1f, 0.95f, 0.9f), 1.5f, 8); s.LookAt(new Vector3(0, 1.1f, -5.6f), new Vector3(0, 0.5f, 0), 33); } // ---------- 12.7 皮肤 ---------- static Texture2D s_Lut; static Texture2D Lut() { if (s_Lut == null) { s_Lut = SkinLutBaker.Generate(); s_Lut.hideFlags = HideFlags.DontSave; } return s_Lut; } [BookShot("ch12-skin-lut", W = 960, H = 300)] static void SkinLutShot(Shot s) { s.Background(Color.white); var m = s.Mat("Universal Render Pipeline/Unlit"); m.SetTexture("_BaseMap", Lut()); var q = s.Quad(m, Vector3.zero, 1); q.transform.localScale = new Vector3(3.2f, 1f, 1); s.Cam.orthographic = true; s.Cam.orthographicSize = 0.5f; s.Cam.transform.position = new Vector3(0, 0, -2); s.Cam.transform.rotation = Quaternion.identity; s.Sun.intensity = 0; } static Material SkinMat(Shot s, int model) { var m = s.Mat("Cookbook/Ch12/Skin"); Kw(m, "_DIFFUSE_", new[] { "LAMBERT", "WRAP", "LUT" }, model); m.SetTexture("_SkinLUT", Lut()); m.SetFloat("_CurvatureScale", 0.5f); m.SetFloat("_Smoothness", 0.55f); return m; } [BookShot("ch12-skin", W = 640, H = 420, Panels = 3)] static void SkinPanels(Shot s) { Studio(s, 0.25f); var amb = RenderSettings.ambientProbe; amb *= 0.35f; RenderSettings.ambientProbe = amb; // 压暗环境光,看清明暗交界 var m = SkinMat(s, s.Panel); s.Sphere(m, new Vector3(-0.55f, 0.6f, 0.3f), 1.2f); s.Sphere(m, new Vector3(0.62f, 0.4f, -0.1f), 0.8f); s.Sphere(m, new Vector3(0.35f, 0.18f, -0.75f), 0.36f); s.Sphere(m, new Vector3(1.0f, 0.12f, -0.75f), 0.24f); s.SunDir(20, 75, 1.8f); // 侧光,让明暗交界线落在画面中间 s.LookAt(new Vector3(0, 1.3f, -3.4f), new Vector3(0.15f, 0.45f, 0), 36); } // ---------- 12.8 薄片透射 ---------- [BookShot("ch12-leaf", W = 640, H = 480, Panels = 3)] static void LeafPanels(Shot s) { s.Background(new Color(0.12f, 0.14f, 0.16f)); var m = s.Mat("Cookbook/Ch12/Translucent"); m.SetFloat("_Transmission", s.Panel == 0 ? 0 : 1); m.SetFloat("_UseThickness", s.Panel == 2 ? 1 : 0); var rnd = new System.Random(5); for (int k = 0; k < 5; k++) { var q = s.Quad(m, new Vector3(-1.1f + k * 0.55f, 0.7f + (k % 2) * 0.35f, k * 0.15f), 1); q.transform.localScale = new Vector3(0.75f, 1.25f, 1); q.transform.rotation = Quaternion.Euler(-10 + (float)rnd.NextDouble() * 20, (float)rnd.NextDouble() * 30 - 15, -25 + k * 12); } // 太阳在叶子背后、略高处,朝相机方向照射 s.SunDir(18, 175, 2.2f); s.LookAt(new Vector3(0, 1.0f, -3.2f), new Vector3(0, 0.9f, 0), 40); } // ---------- 12.9 薄膜干涉 ---------- static Material Film(Shot s, int mode, float thickness, float variation, float baseF0 = 0.3f) { var m = s.Mat("Cookbook/Ch12/ThinFilm"); Kw(m, "_FILM_", new[] { "SPECTRAL", "RGB3", "BELCOUR" }, mode); m.SetFloat("_Thickness", thickness); m.SetFloat("_ThicknessVar", variation); m.SetTexture("_ThicknessTex", s.CoreTex("noise_fbm")); m.SetFloat("_BaseF0", baseF0); m.SetFloat("_FilmIOR", 1.6f); return m; } [BookShot("ch12-thinfilm-row", W = 960, H = 300)] static void ThinFilmRow(Shot s) { Studio(s, 0.12f); s.SphereRow(6, i => Film(s, 0, 150 + i * 100, 0), y: 0.5f, step: 1.05f, scale: 0.95f); s.SunDir(50, -30, 1.2f); s.LookAt(new Vector3(0, 1.2f, -5.0f), new Vector3(0, 0.45f, 0), 34); } [BookShot("ch12-thinfilm", W = 640, H = 480, Panels = 3)] static void ThinFilmPanels(Shot s) { Studio(s, 0.12f); var m = Film(s, s.Panel, 450, 250); m.SetTextureScale("_ThicknessTex", new Vector2(0.5f, 1)); s.Knot(m, new Vector3(0, 0.95f, 0), 1.15f); s.SunDir(35, -30, 1.3f); s.LookAt(new Vector3(0, 1.7f, -3.6f), new Vector3(0, 0.9f, 0), 36); } // ---------- 12.10 材质展台 ---------- [BookShot("ch12-gallery", W = 960, H = 440)] static void Gallery(Shot s) { Studio(s, 0.2f); s.Knot(CarPaint(s, 1, 1), new Vector3(-2.3f, 0.75f, 0.3f), 0.85f); var tor = s.Torus(Brushed(s, 0.85f, 0), new Vector3(-0.95f, 0.55f, 0.0f), 0.48f, 0.2f); tor.transform.rotation = Quaternion.Euler(-65, 0, 0); s.Sphere(Hair(s, 2), new Vector3(0.2f, 0.5f, 0.1f), 1.0f); var pillow = s.Sphere(Velvet(s, 2), new Vector3(1.35f, 0.3f, 0.1f), 1); pillow.transform.localScale = new Vector3(1.05f, 0.6f, 0.9f); s.Sphere(SkinMat(s, 2), new Vector3(2.35f, 0.42f, 0.0f), 0.84f); s.Sphere(Film(s, 0, 420, 200), new Vector3(0.75f, 0.28f, -0.85f), 0.56f); s.SunDir(35, -35, 1.5f); s.Point(new Vector3(-2.2f, 1.8f, -1.2f), new Color(1f, 0.92f, 0.85f), 0.8f, 5); s.LookAt(new Vector3(0, 1.8f, -5.2f), new Vector3(0, 0.45f, 0), 36); } } }