源码 / Ch15/HatchTextures.cs
Ch15/HatchTextures.cs
// 程序化生成色调艺术贴图(Tonal Art Map,TAM):6 个色阶的笔触贴图, // 并打包成两张 RGB 贴图(Hatch0 = 第 1~3 级,Hatch1 = 第 4~6 级)。 // 关键性质是“嵌套”:深一级的贴图包含浅一级的全部笔触,再额外加笔触, // 这样色阶之间混合时笔触不会凭空跳动。 using UnityEngine; namespace Cookbook.Ch15 { public static class HatchTextures { public const int Levels = 6; /// 返回 6 张单通道色阶(1 = 纸白,0 = 墨黑),每张 size×size,可无缝平铺 public static float[][] BuildLevels(int size = 256, int seed = 1234) { var rnd = new System.Random(seed); var levels = new float[Levels][]; var ink = new float[size * size]; // 当前累计的“墨量” for (int i = 0; i < ink.Length; i++) ink[i] = 0f; // 每一级新增的笔触:数量、主方向(度)。前三级只有横向排线,后三级加交叉线 int[] count = { 45, 55, 70, 50, 80, 130 }; float[] angle = { 8, 10, 12, 98, 52, -40 }; for (int lv = 0; lv < Levels; lv++) { for (int k = 0; k < count[lv]; k++) { float cx = (float)rnd.NextDouble() * size, cy = (float)rnd.NextDouble() * size; float len = size * (0.18f + 0.22f * (float)rnd.NextDouble()); float a = (angle[lv] + ((float)rnd.NextDouble() - 0.5f) * 10f) * Mathf.Deg2Rad; float width = 0.9f + 0.6f * (float)rnd.NextDouble(); float dark = 0.55f + 0.35f * (float)rnd.NextDouble(); Stroke(ink, size, cx, cy, Mathf.Cos(a), Mathf.Sin(a), len, width, dark); } var tone = new float[size * size]; for (int i = 0; i < tone.Length; i++) tone[i] = Mathf.Clamp01(1f - ink[i]); levels[lv] = tone; // 拷贝当前累计结果:天然嵌套 } return levels; } // 画一条两端渐细的软边线段;坐标对 size 取模,所以贴图可以无缝平铺 static void Stroke(float[] ink, int size, float cx, float cy, float dx, float dy, float len, float width, float dark) { int steps = Mathf.CeilToInt(len * 2f); for (int s = 0; s <= steps; s++) { float t = (float)s / steps - 0.5f; // -0.5..0.5 float taper = 1f - Mathf.Pow(Mathf.Abs(t) * 2f, 4f); // 笔触两头变细、变淡 float px = cx + dx * t * len, py = cy + dy * t * len; int r = Mathf.CeilToInt(width + 1); for (int oy = -r; oy <= r; oy++) for (int ox = -r; ox <= r; ox++) { int x = Mathf.FloorToInt(px) + ox, y = Mathf.FloorToInt(py) + oy; float d = Mathf.Sqrt((x + 0.5f - px) * (x + 0.5f - px) + (y + 0.5f - py) * (y + 0.5f - py)); float cover = Mathf.Clamp01(width * taper - d + 0.5f); if (cover <= 0) continue; int idx = ((y % size + size) % size) * size + ((x % size + size) % size); // 用 max 而不是累加:同一条线的相邻采样点不会越叠越黑 ink[idx] = Mathf.Max(ink[idx], Mathf.Min(1f, ink[idx] * 0.35f + cover * dark)); } } } /// 把 6 级打包成两张 RGB 贴图,开启 mipmap(远处笔触由 mip 平均,平均灰度保持不变) public static void Pack(float[][] levels, int size, out Texture2D hatch0, out Texture2D hatch1) { hatch0 = new Texture2D(size, size, TextureFormat.RGBA32, true, true) { name = "Hatch0", wrapMode = TextureWrapMode.Repeat, filterMode = FilterMode.Trilinear, anisoLevel = 4 }; hatch1 = new Texture2D(size, size, TextureFormat.RGBA32, true, true) { name = "Hatch1", wrapMode = TextureWrapMode.Repeat, filterMode = FilterMode.Trilinear, anisoLevel = 4 }; var c0 = new Color[size * size]; var c1 = new Color[size * size]; for (int i = 0; i < c0.Length; i++) { c0[i] = new Color(levels[0][i], levels[1][i], levels[2][i], 1); c1[i] = new Color(levels[3][i], levels[4][i], levels[5][i], 1); } hatch0.SetPixels(c0); hatch0.Apply(true); hatch1.SetPixels(c1); hatch1.Apply(true); } /// 6 级并排的预览图(配图用) public static Texture2D Preview(float[][] levels, int size) { int gap = size / 16; int w = size * Levels + gap * (Levels - 1); var tex = new Texture2D(w, size, TextureFormat.RGBA32, false, false) { filterMode = FilterMode.Bilinear }; var px = new Color[w * size]; for (int i = 0; i < px.Length; i++) px[i] = Color.white; for (int lv = 0; lv < Levels; lv++) for (int y = 0; y < size; y++) for (int x = 0; x < size; x++) { float v = levels[lv][y * size + x]; px[y * w + lv * (size + gap) + x] = new Color(v, v, v * 0.97f, 1); } tex.SetPixels(px); tex.Apply(); return tex; } } }