载入中...
搜索中...
未找到
FloorTextures.cpp
浏览该文件的文档.
2
3#include "image/ImageData.h"
8
9#include <algorithm>
10#include <cmath>
11#include <cstdint>
12#include <string>
13#include <string_view>
14#include <utility>
15#include <vector>
16
17namespace eve::procgen {
18namespace {
19
20float smoothstep(float edge0, float edge1, float x) {
21 if (edge0 == edge1) return x < edge0 ? 0.f : 1.f;
22 const float t = std::clamp((x - edge0) / (edge1 - edge0), 0.f, 1.f);
23 return t * t * (3.f - 2.f * t);
24}
25
26struct Rgb {
27 float r = 0.f, g = 0.f, b = 0.f;
28};
29
30Rgb mix(Rgb a, Rgb b, float t) {
31 t = std::clamp(t, 0.f, 1.f);
32 return {a.r + (b.r - a.r) * t, a.g + (b.g - a.g) * t, a.b + (b.b - a.b) * t};
33}
34
35float mixf(float a, float b, float t) {
36 t = std::clamp(t, 0.f, 1.f);
37 return a + (b - a) * t;
38}
39
41struct FloorBake {
42 std::unique_ptr<image::ImageData> albedo;
43 std::vector<float> height;
44 bool seamless = true;
45 int w = 0;
46 int h = 0;
47};
48
49struct FloorPbrDefaults {
50 float roughnessLow = 0.55f;
51 float roughnessHigh = 0.85f;
52 float metallic = 0.f;
53 float normalStrength = 4.f;
54 float aoStrength = 1.f;
55 float heightStrength = 1.f;
56};
57
58void appendPbrParams(RecipeDescriptor& schema, const FloorPbrDefaults& d) {
59 schema.params.push_back(
60 ParamDescriptor::floating("roughnessLow", "Low Roughness", d.roughnessLow, 0.f, 1.f, 0.01f));
61 schema.params.push_back(
62 ParamDescriptor::floating("roughnessHigh", "High Roughness", d.roughnessHigh, 0.f, 1.f, 0.01f));
63 schema.params.push_back(ParamDescriptor::floating("metallic", "Metallic", d.metallic, 0.f, 1.f, 0.01f));
64 schema.params.push_back(
65 ParamDescriptor::floating("normalStrength", "Normal Strength", d.normalStrength, 0.01f, 32.f, 0.05f));
66 schema.params.push_back(
67 ParamDescriptor::floating("aoStrength", "AO Strength", d.aoStrength, 0.f, 5.f, 0.05f));
68 schema.params.push_back(
69 ParamDescriptor::floating("heightStrength", "Height Strength", d.heightStrength, 0.f, 16.f, 0.05f));
70}
71
72FloorPbrDefaults woodPbrDefaults() {
73 // Low height (grooves) → rough; high height (plank face) → smoother.
74 return {0.70f, 0.38f, 0.02f, 6.f, 1.25f, 1.15f};
75}
76
77FloorPbrDefaults tilePbrDefaults() {
78 // Grout is rough; glazed tile faces are smoother.
79 return {0.78f, 0.28f, 0.0f, 5.5f, 1.4f, 1.05f};
80}
81
82std::unique_ptr<PbrTextureSet> assembleFloorPbr(FloorBake bake, const Params& params,
83 const FloorPbrDefaults& defaults, std::string& error) {
84 if (!bake.albedo || bake.height.size() != size_t(bake.w) * size_t(bake.h)) {
85 error = "floor bake missing albedo or height";
86 return {};
87 }
88 const int w = bake.w;
89 const int h = bake.h;
90 const bool seamless = bake.seamless;
91 const float roughnessLow =
92 std::clamp(params.getFloat("roughnessLow", defaults.roughnessLow), 0.f, 1.f);
93 const float roughnessHigh =
94 std::clamp(params.getFloat("roughnessHigh", defaults.roughnessHigh), 0.f, 1.f);
95 const float metallic = std::clamp(params.getFloat("metallic", defaults.metallic), 0.f, 1.f);
96 const float normalStrength =
97 std::max(0.01f, params.getFloat("normalStrength", defaults.normalStrength));
98 const float aoStrength = std::clamp(params.getFloat("aoStrength", defaults.aoStrength), 0.f, 5.f);
99 const float heightStrength =
100 std::max(0.f, params.getFloat("heightStrength", defaults.heightStrength));
101
102 auto set = std::make_unique<PbrTextureSet>();
103 set->albedo = bake.albedo.release();
104 set->normal = heightToNormalImage(bake.height, w, h, normalStrength, seamless).release();
105 if (!set->normal) {
106 error = "floor normal generation failed";
107 return {};
108 }
109
110 std::vector<float> rough(size_t(w * h));
111 std::vector<float> heightMap(size_t(w * h));
112 std::vector<float> ao(size_t(w * h));
113 const int r = std::max(1, std::min(w, h) / 64);
114 for (int y = 0; y < h; ++y) {
115 for (int x = 0; x < w; ++x) {
116 const size_t i = size_t(y) * size_t(w) + size_t(x);
117 const float hh = bake.height[i];
118 rough[i] = roughnessLow + (roughnessHigh - roughnessLow) * hh;
119 heightMap[i] = std::clamp(0.5f + (hh - 0.5f) * heightStrength, 0.f, 1.f);
120
121 float sum = 0.f;
122 int cnt = 0;
123 for (int dy = -r; dy <= r; ++dy) {
124 for (int dx = -r; dx <= r; ++dx) {
125 int xx = x + dx, yy = y + dy;
126 if (seamless) {
127 xx = ((xx % w) + w) % w;
128 yy = ((yy % h) + h) % h;
129 } else {
130 if (xx < 0 || yy < 0 || xx >= w || yy >= h) continue;
131 }
132 sum += bake.height[size_t(yy) * size_t(w) + size_t(xx)];
133 ++cnt;
134 }
135 }
136 const float local = cnt > 0 ? sum / float(cnt) : hh;
137 ao[i] = std::clamp(1.f - aoStrength * std::max(0.f, local - hh), 0.f, 1.f);
138 }
139 }
140 set->roughness = grayscaleImage(rough, w, h).release();
141 set->metallic = grayscaleImage(std::vector<float>(size_t(w * h), metallic), w, h).release();
142 set->height = grayscaleImage(heightMap, w, h).release();
143 set->ao = grayscaleImage(ao, w, h).release();
144 if (!set->roughness || !set->metallic || !set->height || !set->ao) {
145 error = "floor PBR map allocation failed";
146 return {};
147 }
148 return set;
149}
150
151Rgb toneToRgb(std::string_view tone) {
152 if (tone == "walnut") return {0.28f, 0.16f, 0.09f};
153 if (tone == "pine") return {0.72f, 0.58f, 0.34f};
154 if (tone == "cherry") return {0.52f, 0.24f, 0.16f};
155 if (tone == "ebony") return {0.14f, 0.10f, 0.08f};
156 if (tone == "ash") return {0.62f, 0.56f, 0.46f};
157 if (tone == "maple") return {0.78f, 0.62f, 0.38f};
158 if (tone == "teak") return {0.48f, 0.32f, 0.16f};
159 return {0.55f, 0.38f, 0.20f}; // oak
160}
161
162Rgb paletteColorA(std::string_view palette) {
163 if (palette == "terracotta") return {0.72f, 0.38f, 0.22f};
164 if (palette == "slate") return {0.38f, 0.42f, 0.46f};
165 if (palette == "porcelain") return {0.86f, 0.90f, 0.94f};
166 if (palette == "marble") return {0.90f, 0.88f, 0.84f};
167 if (palette == "black") return {0.12f, 0.12f, 0.14f};
168 if (palette == "mosaic") return {0.78f, 0.62f, 0.42f};
169 if (palette == "subway") return {0.92f, 0.93f, 0.94f};
170 if (palette == "encaustic") return {0.86f, 0.78f, 0.62f};
171 if (palette == "jade") return {0.42f, 0.62f, 0.52f};
172 if (palette == "cobalt") return {0.22f, 0.38f, 0.68f};
173 return {0.82f, 0.78f, 0.72f}; // ceramic
174}
175
176Rgb paletteColorB(std::string_view palette) {
177 if (palette == "terracotta") return {0.58f, 0.28f, 0.16f};
178 if (palette == "slate") return {0.26f, 0.30f, 0.34f};
179 if (palette == "porcelain") return {0.55f, 0.68f, 0.82f};
180 if (palette == "marble") return {0.72f, 0.70f, 0.66f};
181 if (palette == "black") return {0.72f, 0.72f, 0.74f};
182 if (palette == "mosaic") return {0.42f, 0.52f, 0.48f};
183 if (palette == "subway") return {0.78f, 0.82f, 0.86f};
184 if (palette == "encaustic") return {0.62f, 0.28f, 0.22f};
185 if (palette == "jade") return {0.78f, 0.86f, 0.74f};
186 if (palette == "cobalt") return {0.90f, 0.86f, 0.72f};
187 return {0.68f, 0.64f, 0.58f};
188}
189
190Rgb groutRgb(std::string_view palette) {
191 if (palette == "terracotta") return {0.42f, 0.32f, 0.24f};
192 if (palette == "slate") return {0.18f, 0.20f, 0.22f};
193 if (palette == "porcelain") return {0.72f, 0.74f, 0.76f};
194 if (palette == "black") return {0.35f, 0.35f, 0.36f};
195 if (palette == "encaustic") return {0.58f, 0.50f, 0.40f};
196 if (palette == "jade") return {0.32f, 0.40f, 0.36f};
197 if (palette == "cobalt") return {0.48f, 0.50f, 0.56f};
198 return {0.55f, 0.52f, 0.48f};
199}
200
201void writePixel(image::ImageData& img, int x, int y, Rgb c) {
202 img.setPixel(x, y, image::ImageData::Colorf{c.r, c.g, c.b, 1.f});
203}
204
205struct WoodOpts {
206 std::string layout = "planks";
207 std::string tone = "oak";
208 int rows = 6;
209 int cols = 4;
210 float gap = 0.04f;
211 float grain = 8.f;
212 float warp = 1.4f;
213 float wear = 0.25f;
214 float stain = 0.15f;
215 float bevel = 0.08f;
216 int colors = 8;
217 int pixelSize = 1;
218};
219
220WoodOpts woodFromParams(const Params& params) {
221 WoodOpts o;
222 o.layout = params.getString("layout", "planks");
223 o.tone = params.getString("tone", "oak");
224 o.rows = std::clamp(params.getInt("rows", 6), 1, 64);
225 o.cols = std::clamp(params.getInt("cols", 4), 1, 64);
226 o.gap = std::clamp(params.getFloat("gap", 0.04f), 0.f, 0.35f);
227 o.grain = std::clamp(params.getFloat("grain", 8.f), 0.5f, 64.f);
228 o.warp = std::clamp(params.getFloat("warp", 1.4f), 0.f, 8.f);
229 o.wear = std::clamp(params.getFloat("wear", 0.25f), 0.f, 1.f);
230 o.stain = std::clamp(params.getFloat("stain", 0.15f), 0.f, 1.f);
231 o.bevel = std::clamp(params.getFloat("bevel", 0.08f), 0.f, 0.4f);
232 o.colors = std::max(2, params.getInt("colors", 8));
233 o.pixelSize = std::max(1, params.getInt("pixelSize", 1));
234 return o;
235}
236
237struct TileOpts {
238 std::string pattern = "square";
239 std::string palette = "ceramic";
240 int tilesX = 4;
241 int tilesY = 4;
242 float grout = 0.06f;
243 float bevel = 0.10f;
244 float glaze = 0.35f;
245 float wear = 0.20f;
246 float speckles = 0.15f;
247 float motif = 0.55f;
248 int colors = 8;
249 int pixelSize = 1;
250};
251
252TileOpts tileFromParams(const Params& params) {
253 TileOpts o;
254 o.pattern = params.getString("pattern", "square");
255 o.palette = params.getString("palette", "ceramic");
256 o.tilesX = std::clamp(params.getInt("tilesX", 4), 1, 64);
257 o.tilesY = std::clamp(params.getInt("tilesY", 4), 1, 64);
258 o.grout = std::clamp(params.getFloat("grout", 0.06f), 0.f, 0.4f);
259 o.bevel = std::clamp(params.getFloat("bevel", 0.10f), 0.f, 0.45f);
260 o.glaze = std::clamp(params.getFloat("glaze", 0.35f), 0.f, 1.f);
261 o.wear = std::clamp(params.getFloat("wear", 0.20f), 0.f, 1.f);
262 o.speckles = std::clamp(params.getFloat("speckles", 0.15f), 0.f, 1.f);
263 o.motif = std::clamp(params.getFloat("motif", 0.55f), 0.f, 1.f);
264 o.colors = std::max(2, params.getInt("colors", 8));
265 o.pixelSize = std::max(1, params.getInt("pixelSize", 1));
266 return o;
267}
268
269float woodGrain(float u, float v, const NoiseField& n, float grain, float warpAmp) {
270 const float w = n.warp(u * 0.35f, v * 0.35f, warpAmp, 3);
271 float g = 0.5f + 0.5f * std::sin((u * grain + w * grain * 0.85f) * 6.2831853f);
272 g = std::pow(std::clamp(g, 0.f, 1.f), 2.2f);
273 const float fleck = n.fbm(u * 2.4f + 1.7f, v * 0.55f, 3);
274 return g * 0.72f + fleck * 0.28f;
275}
276
277struct PlankCell {
278 int idX = 0;
279 int idY = 0;
280 float localU = 0.f; // 0..1 inside plank body (ignoring gap)
281 float localV = 0.f;
282 float edge = 1.f; // 1 = center, 0 = in gap/edge
283 bool inGap = false;
284};
285
286PlankCell samplePlanks(float u, float v, int rows, int cols, float gap, float stagger) {
287 PlankCell cell;
288 const float rowF = v * float(rows);
289 const int row = int(std::floor(rowF));
290 const float off = (row & 1) ? stagger : 0.f;
291 const float colF = u * float(cols) + off;
292 const int col = int(std::floor(colF));
293 const float fu = colF - float(col);
294 const float fv = rowF - float(row);
295 cell.idX = col;
296 cell.idY = row;
297 cell.localU = fu;
298 cell.localV = fv;
299 const float edgeU = std::min(fu, 1.f - fu);
300 const float edgeV = std::min(fv, 1.f - fv);
301 const float edge = std::min(edgeU, edgeV);
302 cell.inGap = edge < gap;
303 cell.edge = cell.inGap ? 0.f : smoothstep(gap, gap + 0.08f, edge);
304 return cell;
305}
306
307PlankCell sampleHerringbone(float u, float v, int rows, int cols, float gap) {
308 // Map UV into diamond-rotated plank cells.
309 const float sx = u * float(cols);
310 const float sy = v * float(rows);
311 const float rx = sx + sy;
312 const float ry = sx - sy;
313 const int ix = int(std::floor(rx));
314 const int iy = int(std::floor(ry));
315 const float fu = rx - float(ix);
316 const float fv = ry - float(iy);
317 PlankCell cell;
318 cell.idX = ix;
319 cell.idY = iy;
320 cell.localU = fu;
321 cell.localV = fv;
322 const float edge = std::min(std::min(fu, 1.f - fu), std::min(fv, 1.f - fv));
323 cell.inGap = edge < gap;
324 cell.edge = cell.inGap ? 0.f : smoothstep(gap, gap + 0.08f, edge);
325 return cell;
326}
327
328PlankCell sampleParquet(float u, float v, int rows, int cols, float gap) {
329 // Basket / parquet blocks: 2x2 rotated plank groups.
330 const float bx = u * float(std::max(1, cols / 2));
331 const float by = v * float(std::max(1, rows / 2));
332 const int bxI = int(std::floor(bx));
333 const int byI = int(std::floor(by));
334 const float fx = bx - float(bxI);
335 const float fy = by - float(byI);
336 const bool rot = ((bxI + byI) & 1) != 0;
337 float pu = rot ? fy : fx;
338 float pv = rot ? fx : fy;
339 const int strips = 4;
340 pu *= float(strips);
341 const int strip = int(std::floor(pu));
342 const float su = pu - float(strip);
343 PlankCell cell;
344 cell.idX = bxI * 16 + strip;
345 cell.idY = byI;
346 cell.localU = su;
347 cell.localV = pv;
348 const float edgeU = std::min(su, 1.f - su);
349 const float edgeV = std::min(pv, 1.f - pv);
350 const float edge = std::min(edgeU, edgeV);
351 // Soft block boundary.
352 const float blockEdge = std::min(std::min(fx, 1.f - fx), std::min(fy, 1.f - fy));
353 cell.inGap = edge < gap || blockEdge < gap * 0.85f;
354 cell.edge = cell.inGap ? 0.f : smoothstep(gap, gap + 0.08f, std::min(edge, blockEdge));
355 return cell;
356}
357
358PlankCell sampleDiagonal(float u, float v, int rows, int cols, float gap) {
359 // 45-degree diagonal planks.
360 const float sx = (u + v) * 0.5f * float(cols);
361 const float sy = (u - v + 1.f) * 0.5f * float(rows);
362 const int ix = int(std::floor(sx));
363 const int iy = int(std::floor(sy));
364 const float fu = sx - float(ix);
365 const float fv = sy - float(iy);
366 PlankCell cell;
367 cell.idX = ix;
368 cell.idY = iy;
369 cell.localU = fu;
370 cell.localV = fv;
371 const float edge = std::min(std::min(fu, 1.f - fu), std::min(fv, 1.f - fv));
372 cell.inGap = edge < gap;
373 cell.edge = cell.inGap ? 0.f : smoothstep(gap, gap + 0.08f, edge);
374 return cell;
375}
376
377PlankCell sampleLadder(float u, float v, int rows, int cols, float gap) {
378 // Ladder / finger parquet: horizontal bands of short vertical strips.
379 const float bandF = v * float(std::max(1, rows));
380 const int band = int(std::floor(bandF));
381 const float fv = bandF - float(band);
382 const float stripF = u * float(std::max(1, cols * 2));
383 const int strip = int(std::floor(stripF));
384 const float fu = stripF - float(strip);
385 PlankCell cell;
386 cell.idX = strip;
387 cell.idY = band;
388 cell.localU = fu;
389 cell.localV = fv;
390 const float edgeU = std::min(fu, 1.f - fu);
391 const float edgeV = std::min(fv, 1.f - fv);
392 const float edge = std::min(edgeU, edgeV);
393 cell.inGap = edge < gap;
394 cell.edge = cell.inGap ? 0.f : smoothstep(gap, gap + 0.08f, edge);
395 return cell;
396}
397
398PlankCell sampleVersailles(float u, float v, int rows, int cols, float gap) {
399 // Versailles parquet: square modules with a framed cross of planks.
400 const float mx = u * float(std::max(1, cols / 2));
401 const float my = v * float(std::max(1, rows / 2));
402 const int ix = int(std::floor(mx));
403 const int iy = int(std::floor(my));
404 const float fx = mx - float(ix);
405 const float fy = my - float(iy);
406 // Distance to module frame and to the central cross arms.
407 const float frame = std::min(std::min(fx, 1.f - fx), std::min(fy, 1.f - fy));
408 const float arm = std::min(std::fabs(fx - 0.5f), std::fabs(fy - 0.5f));
409 const bool onArm = arm < 0.12f;
410 const bool onFrame = frame < 0.10f;
411 PlankCell cell;
412 cell.idX = ix * 8 + (onArm ? 1 : 0) + (onFrame ? 2 : 0);
413 cell.idY = iy;
414 if (onFrame) {
415 cell.localU = frame / 0.10f;
416 cell.localV = (std::fabs(fx - 0.5f) < std::fabs(fy - 0.5f)) ? fy : fx;
417 } else if (onArm) {
418 const bool horiz = std::fabs(fy - 0.5f) < std::fabs(fx - 0.5f);
419 cell.localU = horiz ? fx : fy;
420 cell.localV = horiz ? (fy - 0.5f + 0.12f) / 0.24f : (fx - 0.5f + 0.12f) / 0.24f;
421 } else {
422 // Corner filler planks oriented by quadrant.
423 const bool horiz = ((fx < 0.5f) == (fy < 0.5f));
424 cell.localU = horiz ? fx : fy;
425 cell.localV = horiz ? fy : fx;
426 cell.idX += 4;
427 }
428 const float edge = onFrame ? frame : (onArm ? arm : std::min(frame, arm));
429 cell.inGap = edge < gap * 0.7f;
430 cell.edge = cell.inGap ? 0.f : smoothstep(gap * 0.7f, gap * 0.7f + 0.08f, edge);
431 return cell;
432}
433
434Rgb shadeWood(const WoodOpts& opts, const PlankCell& cell, float grain, const NoiseField& n, float u, float v) {
435 Rgb base = toneToRgb(opts.tone);
436 // Per-plank tone variation.
437 const float plankVar =
438 n.hash01(cell.idX * 17 + 3, cell.idY * 29 + 7) * 0.22f - 0.11f;
439 base.r = std::clamp(base.r + plankVar, 0.f, 1.f);
440 base.g = std::clamp(base.g + plankVar * 0.85f, 0.f, 1.f);
441 base.b = std::clamp(base.b + plankVar * 0.55f, 0.f, 1.f);
442
443 Rgb dark = {base.r * 0.45f, base.g * 0.42f, base.b * 0.38f};
444 Rgb lit = {std::min(1.f, base.r * 1.18f), std::min(1.f, base.g * 1.12f), std::min(1.f, base.b * 1.05f)};
445 Rgb col = mix(dark, lit, grain);
446
447 // Bevel / gap: darker recessed seams.
448 const float bevelAmt = cell.inGap ? 0.f : smoothstep(0.f, opts.bevel + 1e-4f, cell.edge);
449 col = mix(Rgb{0.08f, 0.05f, 0.03f}, col, cell.inGap ? 0.18f : (0.55f + 0.45f * bevelAmt));
450
451 // Wear near edges + scratches.
452 const float edgeWear = (1.f - cell.edge) * opts.wear;
453 const float scratch =
454 smoothstep(0.72f, 0.92f, n.valueNoise(u * 18.f + float(cell.idX), v * 3.2f + float(cell.idY)));
455 col = mix(col, lit, edgeWear * 0.35f + scratch * opts.wear * 0.25f);
456
457 // Stain darkens overall.
458 col.r *= 1.f - opts.stain * 0.55f;
459 col.g *= 1.f - opts.stain * 0.58f;
460 col.b *= 1.f - opts.stain * 0.50f;
461 return col;
462}
463
464float woodDisplacement(const WoodOpts& opts, const PlankCell& cell, float grain, const NoiseField& n, float u,
465 float v) {
466 if (cell.inGap) {
467 const float grit = n.valueNoise(u * 36.f, v * 36.f);
468 return 0.08f + grit * 0.05f;
469 }
470 const float bevelAmt = smoothstep(0.f, opts.bevel + 1e-4f, cell.edge);
471 const float micro = grain * 0.22f + n.fbm(u * 5.5f + float(cell.idX), v * 1.8f + float(cell.idY), 2) * 0.06f;
472 const float scratch =
473 smoothstep(0.78f, 0.95f, n.valueNoise(u * 18.f + float(cell.idX), v * 3.2f + float(cell.idY))) *
474 opts.wear * 0.08f;
475 return mixf(0.20f, 0.62f + micro - scratch, 0.28f + 0.72f * bevelAmt);
476}
477
478FloorBake bakeWoodFloor(const Params& params, std::string& error) {
479 FloorBake bake;
480 const int w = std::clamp(params.getWidth() > 0 ? params.getWidth() : 256, 8, 4096);
481 const int h = std::clamp(params.getHeight() > 0 ? params.getHeight() : 256, 8, 4096);
482 if (w > 4096 || h > 4096) {
483 error = "texture size too large (max 4096)";
484 return bake;
485 }
486 const WoodOpts opts = woodFromParams(params);
487 NoiseField n;
488 n.seed = params.getSeed();
489 const bool seamless = params.getInt("seamless", 1) != 0;
490 if (seamless) {
491 n.periodX = std::max(1, opts.cols);
492 n.periodY = std::max(1, opts.rows);
493 }
494
495 bake.w = w;
496 bake.h = h;
497 bake.seamless = seamless;
498 bake.height.assign(size_t(w) * size_t(h), 0.5f);
499 bake.albedo = std::make_unique<image::ImageData>(w, h, "RGBA8");
500 const int px = opts.pixelSize;
501 for (int y = 0; y < h; ++y) {
502 const int by = (y / px) * px;
503 for (int x = 0; x < w; ++x) {
504 const int bx = (x / px) * px;
505 const float u = (float(bx) + 0.5f) / float(w);
506 const float v = (float(by) + 0.5f) / float(h);
507
508 PlankCell cell;
509 float grainU = u;
510 float grainV = v;
511 if (opts.layout == "herringbone" || opts.layout == "chevron") {
512 cell = sampleHerringbone(u, v, opts.rows, opts.cols, opts.gap);
513 grainU = (u + v) * 0.5f;
514 grainV = (u - v) * 0.5f + 0.5f;
515 } else if (opts.layout == "parquet" || opts.layout == "basket") {
516 cell = sampleParquet(u, v, opts.rows, opts.cols, opts.gap);
517 grainU = cell.localU;
518 grainV = cell.localV;
519 } else if (opts.layout == "diagonal") {
520 cell = sampleDiagonal(u, v, opts.rows, opts.cols, opts.gap);
521 grainU = (u + v) * 0.5f;
522 grainV = (u - v) * 0.5f + 0.5f;
523 } else if (opts.layout == "ladder" || opts.layout == "finger") {
524 cell = sampleLadder(u, v, opts.rows, opts.cols, opts.gap);
525 grainU = cell.localV;
526 grainV = cell.localU;
527 } else if (opts.layout == "versailles") {
528 cell = sampleVersailles(u, v, opts.rows, opts.cols, opts.gap);
529 grainU = cell.localU;
530 grainV = cell.localV;
531 } else {
532 // planks (default) and staggered.
533 const float stagger = (opts.layout == "staggered") ? 0.5f : 0.f;
534 cell = samplePlanks(u, v, opts.rows, opts.cols, opts.gap, stagger);
535 grainU = cell.localU + float(cell.idX);
536 grainV = cell.localV;
537 }
538
539 const float grain =
540 woodGrain(grainU * opts.grain * 0.35f, grainV * opts.grain * 0.12f, n, opts.grain * 0.55f, opts.warp);
541 Rgb col = shadeWood(opts, cell, grain, n, u, v);
542
543 // Band quantization for a slightly stylized look when colors is low.
544 if (opts.colors < 64) {
545 const float bands = float(opts.colors);
546 col.r = std::floor(col.r * bands) / bands;
547 col.g = std::floor(col.g * bands) / bands;
548 col.b = std::floor(col.b * bands) / bands;
549 }
550 writePixel(*bake.albedo, x, y, col);
551 bake.height[size_t(y) * size_t(w) + size_t(x)] =
552 woodDisplacement(opts, cell, grain, n, u, v);
553 }
554 }
555 return bake;
556}
557
558std::unique_ptr<image::ImageData> makeWoodImage(const Params& params, std::string& error) {
559 FloorBake bake = bakeWoodFloor(params, error);
560 return std::move(bake.albedo);
561}
562
563struct TileCell {
564 int idX = 0;
565 int idY = 0;
566 float localU = 0.f;
567 float localV = 0.f;
568 float edge = 1.f;
569 bool inGrout = false;
570 int colorAlt = 0; // 0/1 for checker-like patterns
571};
572
573TileCell sampleSquare(float u, float v, int tilesX, int tilesY, float grout) {
574 TileCell c;
575 const float fx = u * float(tilesX);
576 const float fy = v * float(tilesY);
577 c.idX = int(std::floor(fx));
578 c.idY = int(std::floor(fy));
579 c.localU = fx - float(c.idX);
580 c.localV = fy - float(c.idY);
581 const float e = std::min(std::min(c.localU, 1.f - c.localU), std::min(c.localV, 1.f - c.localV));
582 c.inGrout = e < grout;
583 c.edge = c.inGrout ? 0.f : smoothstep(grout, grout + 0.08f, e);
584 c.colorAlt = (c.idX + c.idY) & 1;
585 return c;
586}
587
588TileCell sampleDiamond(float u, float v, int tilesX, int tilesY, float grout) {
589 const float sx = (u - 0.5f) * float(tilesX);
590 const float sy = (v - 0.5f) * float(tilesY);
591 const float rx = sx + sy;
592 const float ry = sx - sy;
593 TileCell c;
594 c.idX = int(std::floor(rx));
595 c.idY = int(std::floor(ry));
596 c.localU = rx - float(c.idX);
597 c.localV = ry - float(c.idY);
598 const float e = std::min(std::min(c.localU, 1.f - c.localU), std::min(c.localV, 1.f - c.localV));
599 c.inGrout = e < grout;
600 c.edge = c.inGrout ? 0.f : smoothstep(grout, grout + 0.08f, e);
601 c.colorAlt = (c.idX + c.idY) & 1;
602 return c;
603}
604
605TileCell sampleHex(float u, float v, int tilesX, int tilesY, float grout) {
606 // Axial-ish hex lattice in UV.
607 const float size = 1.f / float(std::max(1, tilesX));
608 const float q = (2.f / 3.f * u) / size;
609 const float r = (-1.f / 3.f * u + std::sqrt(3.f) / 3.f * v) / size;
610 // Cube round.
611 float x = q, z = r, y = -x - z;
612 const float rx = std::round(x), ry = std::round(y), rz = std::round(z);
613 const float xDiff = std::fabs(rx - x), yDiff = std::fabs(ry - y), zDiff = std::fabs(rz - z);
614 float qx = rx, qz = rz;
615 if (xDiff > yDiff && xDiff > zDiff)
616 qx = -ry - rz;
617 else if (yDiff > zDiff)
618 qz = -rx - ry;
619 TileCell c;
620 c.idX = int(qx);
621 c.idY = int(qz);
622 // Approximate edge distance via fractional axial coords.
623 const float fx = q - qx;
624 const float fz = r - qz;
625 const float fy = (-q - r) - (-qx - qz);
626 const float e = 1.f - std::max(std::fabs(fx), std::max(std::fabs(fy), std::fabs(fz)));
627 c.localU = fx + 0.5f;
628 c.localV = fz + 0.5f;
629 c.inGrout = e < grout * 1.6f;
630 c.edge = c.inGrout ? 0.f : smoothstep(grout * 1.6f, grout * 1.6f + 0.08f, e);
631 c.colorAlt = (c.idX + c.idY * 3) & 1;
632 (void)tilesY;
633 return c;
634}
635
636TileCell sampleSubway(float u, float v, int tilesX, int tilesY, float grout) {
637 // Wide bricks, half-offset rows.
638 TileCell c;
639 const float fy = v * float(tilesY);
640 c.idY = int(std::floor(fy));
641 const float off = (c.idY & 1) ? 0.5f : 0.f;
642 const float fx = u * float(tilesX) + off;
643 c.idX = int(std::floor(fx));
644 c.localU = fx - float(c.idX);
645 c.localV = fy - float(c.idY);
646 const float e = std::min(std::min(c.localU, 1.f - c.localU), std::min(c.localV, 1.f - c.localV));
647 c.inGrout = e < grout;
648 c.edge = c.inGrout ? 0.f : smoothstep(grout, grout + 0.08f, e);
649 c.colorAlt = c.idX & 1;
650 return c;
651}
652
653TileCell sampleMosaic(float u, float v, int tilesX, int tilesY, float grout, const NoiseField& n) {
654 // Voronoi-like irregular mosaic chips.
655 const float sx = u * float(tilesX);
656 const float sy = v * float(tilesY);
657 const int cx0 = int(std::floor(sx));
658 const int cy0 = int(std::floor(sy));
659 float best = 1e9f;
660 int bx = cx0, by = cy0;
661 float second = 1e9f;
662 for (int oy = -1; oy <= 1; ++oy) {
663 for (int ox = -1; ox <= 1; ++ox) {
664 const int cx = cx0 + ox;
665 const int cy = cy0 + oy;
666 const float px = float(cx) + n.hash01(cx, cy);
667 const float py = float(cy) + n.hash01(cx * 7 + 3, cy * 13 + 5);
668 const float dx = sx - px;
669 const float dy = sy - py;
670 const float d = dx * dx + dy * dy;
671 if (d < best) {
672 second = best;
673 best = d;
674 bx = cx;
675 by = cy;
676 } else if (d < second) {
677 second = d;
678 }
679 }
680 }
681 TileCell c;
682 c.idX = bx;
683 c.idY = by;
684 c.localU = sx - float(bx);
685 c.localV = sy - float(by);
686 const float d1 = std::sqrt(best);
687 const float d2 = std::sqrt(second);
688 const float e = d2 - d1;
689 c.inGrout = e < grout * 0.55f;
690 c.edge = c.inGrout ? 0.f : smoothstep(grout * 0.55f, grout * 0.55f + 0.08f, e);
691 c.colorAlt = int(n.hash01(bx, by) * 3.f);
692 return c;
693}
694
695TileCell sampleBasketTile(float u, float v, int tilesX, int tilesY, float grout) {
696 // Basketweave: alternating 2x1 / 1x2 rectangles.
697 const float bx = u * float(std::max(1, tilesX / 2));
698 const float by = v * float(std::max(1, tilesY / 2));
699 const int ix = int(std::floor(bx));
700 const int iy = int(std::floor(by));
701 const float fx = bx - float(ix);
702 const float fy = by - float(iy);
703 const bool horizontal = ((ix + iy) & 1) == 0;
704 TileCell c;
705 if (horizontal) {
706 const float strip = fy * 2.f;
707 c.idY = iy * 2 + int(std::floor(strip));
708 c.idX = ix;
709 c.localU = fx;
710 c.localV = strip - std::floor(strip);
711 } else {
712 const float strip = fx * 2.f;
713 c.idX = ix * 2 + int(std::floor(strip));
714 c.idY = iy;
715 c.localU = strip - std::floor(strip);
716 c.localV = fy;
717 }
718 const float e = std::min(std::min(c.localU, 1.f - c.localU), std::min(c.localV, 1.f - c.localV));
719 const float blockEdge =
720 std::min(std::min(fx, 1.f - fx), std::min(fy, 1.f - fy));
721 c.inGrout = e < grout || blockEdge < grout * 0.9f;
722 c.edge = c.inGrout ? 0.f : smoothstep(grout, grout + 0.08f, std::min(e, blockEdge));
723 c.colorAlt = (c.idX + c.idY) & 1;
724 return c;
725}
726
727TileCell sampleOctagon(float u, float v, int tilesX, int tilesY, float grout) {
728 // Classic octagon field with small diamond/square inserts at corners.
729 const float sx = u * float(tilesX);
730 const float sy = v * float(tilesY);
731 const int ix = int(std::floor(sx));
732 const int iy = int(std::floor(sy));
733 const float fx = sx - float(ix);
734 const float fy = sy - float(iy);
735 // Manhattanish clipped square → octagon: cut corners where |fx-0.5|+|fy-0.5| is large.
736 const float manhattan = std::fabs(fx - 0.5f) + std::fabs(fy - 0.5f);
737 const bool insert = manhattan > 0.68f;
738 TileCell c;
739 if (insert) {
740 // Map to nearest corner insert cell.
741 const int cx = ix + (fx > 0.5f ? 1 : 0);
742 const int cy = iy + (fy > 0.5f ? 1 : 0);
743 c.idX = cx * 2 + 1;
744 c.idY = cy * 2 + 1;
745 c.localU = std::fmod(fx + 0.5f, 1.f);
746 c.localV = std::fmod(fy + 0.5f, 1.f);
747 c.colorAlt = 1;
748 const float e = std::min(std::min(c.localU, 1.f - c.localU), std::min(c.localV, 1.f - c.localV));
749 c.inGrout = e < grout * 1.1f;
750 c.edge = c.inGrout ? 0.f : smoothstep(grout, grout + 0.08f, e);
751 } else {
752 c.idX = ix * 2;
753 c.idY = iy * 2;
754 c.localU = fx;
755 c.localV = fy;
756 c.colorAlt = 0;
757 const float box = std::min(std::min(fx, 1.f - fx), std::min(fy, 1.f - fy));
758 const float cut = 0.68f - manhattan;
759 const float e = std::min(box, cut);
760 c.inGrout = e < grout;
761 c.edge = c.inGrout ? 0.f : smoothstep(grout, grout + 0.08f, e);
762 }
763 return c;
764}
765
766TileCell sampleFishscale(float u, float v, int tilesX, int tilesY, float grout) {
767 // Overlapping half-drop circular scales.
768 const float sx = u * float(tilesX);
769 const float sy = v * float(tilesY);
770 const int row = int(std::floor(sy));
771 const float off = (row & 1) ? 0.5f : 0.f;
772 const float colF = sx + off;
773 const int col = int(std::floor(colF));
774 float best = 1e9f;
775 int bx = col, by = row;
776 for (int oy = -1; oy <= 1; ++oy) {
777 for (int ox = -1; ox <= 1; ++ox) {
778 const int cy = row + oy;
779 const float o = (cy & 1) ? 0.5f : 0.f;
780 const int cx = int(std::floor(sx + o)) + ox;
781 const float px = float(cx) + 0.5f - o;
782 const float py = float(cy) + 0.55f;
783 const float dx = (sx - px);
784 const float dy = (sy - py) * 1.15f;
785 const float d = std::sqrt(dx * dx + dy * dy);
786 if (d < best) {
787 best = d;
788 bx = cx;
789 by = cy;
790 }
791 }
792 }
793 TileCell c;
794 c.idX = bx;
795 c.idY = by;
796 c.localU = 0.5f + (sx - (float(bx) + 0.5f - ((by & 1) ? 0.5f : 0.f)));
797 c.localV = best;
798 c.colorAlt = (bx + by) & 1;
799 c.inGrout = best > 0.52f - grout * 0.35f;
800 c.edge = c.inGrout ? 0.f : smoothstep(0.52f - grout * 0.35f - 0.08f, 0.52f - grout * 0.35f, best);
801 // Invert edge so center of scale is high.
802 if (!c.inGrout) c.edge = 1.f - c.edge;
803 (void)tilesY;
804 return c;
805}
806
807TileCell samplePinwheel(float u, float v, int tilesX, int tilesY, float grout) {
808 // Pinwheel / windmill: four right triangles around a center square.
809 const float sx = u * float(std::max(1, tilesX / 2));
810 const float sy = v * float(std::max(1, tilesY / 2));
811 const int ix = int(std::floor(sx));
812 const int iy = int(std::floor(sy));
813 const float fx = sx - float(ix);
814 const float fy = sy - float(iy);
815 TileCell c;
816 c.idX = ix;
817 c.idY = iy;
818 // Center square.
819 if (fx > 0.3f && fx < 0.7f && fy > 0.3f && fy < 0.7f) {
820 c.localU = (fx - 0.3f) / 0.4f;
821 c.localV = (fy - 0.3f) / 0.4f;
822 c.colorAlt = 0;
823 const float e = std::min(std::min(c.localU, 1.f - c.localU), std::min(c.localV, 1.f - c.localV));
824 c.inGrout = e < grout;
825 c.edge = c.inGrout ? 0.f : smoothstep(grout, grout + 0.08f, e);
826 return c;
827 }
828 // Four triangular blades classified by diagonal.
829 int blade = 0;
830 if (fy < fx && fy < 1.f - fx)
831 blade = 0; // bottom
832 else if (fy >= fx && fy < 1.f - fx)
833 blade = 1; // left-ish → right of bottom-left diagonal
834 else if (fy >= 1.f - fx && fy >= fx)
835 blade = 2; // top
836 else
837 blade = 3;
838 c.idX += blade * 17;
839 c.colorAlt = 1 + (blade & 1);
840 c.localU = fx;
841 c.localV = fy;
842 const float diagDist = std::min(std::fabs(fy - fx), std::fabs(fy - (1.f - fx)));
843 const float box = std::min(std::min(fx, 1.f - fx), std::min(fy, 1.f - fy));
844 const float e = std::min(diagDist, box);
845 c.inGrout = e < grout * 0.85f;
846 c.edge = c.inGrout ? 0.f : smoothstep(grout * 0.85f, grout * 0.85f + 0.08f, e);
847 return c;
848}
849
850TileCell sampleStar(float u, float v, int tilesX, int tilesY, float grout) {
851 // Simplified Moroccan 8-point star lattice on a square grid.
852 const float sx = u * float(tilesX);
853 const float sy = v * float(tilesY);
854 const int ix = int(std::floor(sx));
855 const int iy = int(std::floor(sy));
856 const float fx = sx - float(ix) - 0.5f;
857 const float fy = sy - float(iy) - 0.5f;
858 const float ax = std::fabs(fx);
859 const float ay = std::fabs(fy);
860 // Star body: diamond (manhattan) mixed with axis-aligned square.
861 const float diamond = ax + ay;
862 const float square = std::max(ax, ay);
863 const float star = std::min(diamond * 0.92f, square * 1.35f);
864 TileCell c;
865 c.idX = ix;
866 c.idY = iy;
867 c.localU = fx + 0.5f;
868 c.localV = fy + 0.5f;
869 c.colorAlt = (diamond < 0.55f) ? 0 : 1;
870 c.inGrout = star > 0.62f - grout * 0.4f;
871 c.edge = c.inGrout ? 0.f : smoothstep(0.f, 0.12f, 0.62f - grout * 0.4f - star);
872 return c;
873}
874
875TileCell sampleCobble(float u, float v, int tilesX, int tilesY, float grout, const NoiseField& n) {
876 // Rounded cobblestones via voronoi with circular falloff.
877 TileCell base = sampleMosaic(u, v, tilesX, tilesY, grout * 1.4f, n);
878 const float sx = u * float(tilesX);
879 const float sy = v * float(tilesY);
880 const float px = float(base.idX) + n.hash01(base.idX, base.idY);
881 const float py = float(base.idY) + n.hash01(base.idX * 7 + 3, base.idY * 13 + 5);
882 const float dx = sx - px;
883 const float dy = sy - py;
884 const float d = std::sqrt(dx * dx + dy * dy);
885 base.localU = dx + 0.5f;
886 base.localV = dy + 0.5f;
887 base.inGrout = d > 0.42f - grout * 0.25f;
888 base.edge = base.inGrout ? 0.f : smoothstep(0.f, 0.12f, 0.42f - grout * 0.25f - d);
889 return base;
890}
891
892TileCell sampleArabesque(float u, float v, int tilesX, int tilesY, float grout) {
893 // Interlocking scalloped arabesque: circle packing with half-offset rows.
894 const float sx = u * float(tilesX);
895 const float sy = v * float(tilesY);
896 const int row = int(std::floor(sy));
897 const float off = (row & 1) ? 0.5f : 0.f;
898 float best = 1e9f;
899 int bx = 0, by = row;
900 for (int oy = -1; oy <= 1; ++oy) {
901 for (int ox = -1; ox <= 1; ++ox) {
902 const int cy = row + oy;
903 const float o = (cy & 1) ? 0.5f : 0.f;
904 const int cx = int(std::floor(sx + o)) + ox;
905 const float px = float(cx) + 0.5f - o;
906 const float py = float(cy) + 0.5f;
907 const float dx = sx - px;
908 const float dy = sy - py;
909 const float d = std::sqrt(dx * dx + dy * dy);
910 if (d < best) {
911 best = d;
912 bx = cx;
913 by = cy;
914 }
915 }
916 }
917 TileCell c;
918 c.idX = bx;
919 c.idY = by;
920 c.localU = 0.5f;
921 c.localV = best;
922 c.colorAlt = (bx + by) & 1;
923 // Petal edge via radial rings.
924 const float ring = std::fabs(std::sin(best * 6.28318f * 1.5f));
925 c.inGrout = best > 0.48f - grout * 0.3f || ring < grout * 1.8f;
926 c.edge = c.inGrout ? 0.f : smoothstep(0.f, 0.1f, 0.48f - best) * (0.4f + 0.6f * ring);
927 (void)off;
928 return c;
929}
930
931Rgb shadeTile(const TileOpts& opts, const TileCell& cell, const NoiseField& n, float u, float v) {
932 Rgb a = paletteColorA(opts.palette);
933 Rgb b = paletteColorB(opts.palette);
934 Rgb body;
935 if (opts.pattern == "checker" || opts.pattern == "pinwheel" || opts.pattern == "windmill") {
936 body = (cell.colorAlt == 0) ? a : b;
937 } else if (opts.pattern == "mosaic" || opts.pattern == "cobble" || opts.pattern == "terrazzo") {
938 const float t = n.hash01(cell.idX * 3 + 1, cell.idY * 5 + 2);
939 body = mix(a, b, t);
940 } else if (opts.pattern == "star" || opts.pattern == "octagon") {
941 body = (cell.colorAlt == 0) ? a : mix(a, b, 0.65f);
942 } else {
943 const float tileVar = n.hash01(cell.idX + 11, cell.idY + 19) * 0.12f - 0.06f;
944 body = a;
945 body.r = std::clamp(body.r + tileVar, 0.f, 1.f);
946 body.g = std::clamp(body.g + tileVar, 0.f, 1.f);
947 body.b = std::clamp(body.b + tileVar * 0.8f, 0.f, 1.f);
948 if (opts.motif > 0.01f && opts.pattern != "square" && opts.pattern != "subway" &&
949 opts.pattern != "stack") {
950 body = mix(body, b, opts.motif * 0.35f * float(cell.colorAlt));
951 }
952 }
953
954 // Decorative motif: concentric / diamond / star inset on ceramic faces.
955 if (!cell.inGrout && opts.motif > 0.05f) {
956 const float dx = cell.localU - 0.5f;
957 const float dy = cell.localV - 0.5f;
958 float m = 0.f;
959 if (opts.pattern == "diamond" || opts.pattern == "star") {
960 m = 1.f - (std::fabs(dx) + std::fabs(dy)) * 1.6f;
961 } else if (opts.pattern == "square" || opts.pattern == "checker" || opts.pattern == "octagon") {
962 m = 1.f - std::sqrt(dx * dx + dy * dy) * 2.2f;
963 } else if (opts.pattern == "arabesque") {
964 m = 1.f - cell.localV * 1.8f;
965 }
966 if (m > 0.f) {
967 m = smoothstep(0.15f, 0.55f, m) * opts.motif;
968 body = mix(body, b, m * 0.55f);
969 const float ring = smoothstep(0.02f, 0.f, std::fabs(m - 0.45f));
970 body = mix(body, mix(a, b, 0.7f), ring * opts.motif * 0.4f);
971 }
972 }
973
974 // Terrazzo chips: high-frequency multi-hue flecks.
975 if (opts.pattern == "terrazzo" && !cell.inGrout) {
976 const float chip = n.valueNoise(u * 48.f + float(cell.idX), v * 48.f + float(cell.idY));
977 const float chip2 =
978 n.valueNoise(u * 31.f + 4.f + float(cell.idY), v * 31.f + 2.f + float(cell.idX));
979 if (chip > 0.72f) body = mix(body, b, 0.55f + opts.speckles * 0.3f);
980 if (chip2 > 0.80f) body = mix(body, Rgb{0.92f, 0.90f, 0.86f}, 0.45f);
981 if (chip < 0.18f) body = mix(body, Rgb{0.25f, 0.24f, 0.22f}, 0.35f);
982 }
983
984 // Speckles / glaze mottling.
985 const float speck = n.valueNoise(u * 22.f + float(cell.idX), v * 22.f + float(cell.idY));
986 body = mix(body, mix(body, Rgb{1.f, 1.f, 1.f}, 0.35f),
987 smoothstep(0.62f, 0.88f, speck) * opts.speckles * 0.5f);
988 const float glazeNoise = n.fbm(u * 3.5f, v * 3.5f, 3);
989 body = mix(body, mix(body, Rgb{1.f, 1.f, 1.f}, 0.25f), glazeNoise * opts.glaze * 0.35f);
990
991 // Wear darkens center slightly, lightens edges.
992 const float wearEdge = (1.f - cell.edge) * opts.wear;
993 body = mix(body, mix(body, Rgb{0.95f, 0.93f, 0.90f}, 0.4f), wearEdge * 0.45f);
994
995 Rgb grout = groutRgb(opts.palette);
996 if (cell.inGrout) {
997 const float grit = n.valueNoise(u * 40.f, v * 40.f);
998 grout = mix(grout, Rgb{grout.r * 0.7f, grout.g * 0.7f, grout.b * 0.7f}, grit * 0.35f);
999 return grout;
1000 }
1001
1002 const float bevelAmt = smoothstep(0.f, opts.bevel + 1e-4f, cell.edge);
1003 return mix(grout, body, 0.35f + 0.65f * bevelAmt);
1004}
1005
1006float tileDisplacement(const TileOpts& opts, const TileCell& cell, const NoiseField& n, float u, float v) {
1007 if (cell.inGrout) {
1008 const float grit = n.valueNoise(u * 40.f, v * 40.f);
1009 return 0.10f + grit * 0.06f;
1010 }
1011 const float bevelAmt = smoothstep(0.f, opts.bevel + 1e-4f, cell.edge);
1012 const float glazeLift = opts.glaze * 0.10f;
1013 const float wearDip =
1014 (1.f - cell.edge) * opts.wear * 0.06f +
1015 smoothstep(0.7f, 0.92f, n.valueNoise(u * 14.f, v * 14.f)) * opts.wear * 0.04f;
1016 float hh = mixf(0.26f, 0.70f + glazeLift - wearDip, 0.30f + 0.70f * bevelAmt);
1017 if (opts.motif > 0.05f) {
1018 const float dx = cell.localU - 0.5f;
1019 const float dy = cell.localV - 0.5f;
1020 float m = 0.f;
1021 if (opts.pattern == "diamond" || opts.pattern == "star") {
1022 m = 1.f - (std::fabs(dx) + std::fabs(dy)) * 1.6f;
1023 } else if (opts.pattern == "square" || opts.pattern == "checker" || opts.pattern == "octagon") {
1024 m = 1.f - std::sqrt(dx * dx + dy * dy) * 2.2f;
1025 }
1026 if (m > 0.15f && m < 0.55f) hh -= opts.motif * 0.04f;
1027 }
1028 return std::clamp(hh, 0.f, 1.f);
1029}
1030
1031FloorBake bakeTileFloor(const Params& params, std::string& error) {
1032 FloorBake bake;
1033 const int w = std::clamp(params.getWidth() > 0 ? params.getWidth() : 256, 8, 4096);
1034 const int h = std::clamp(params.getHeight() > 0 ? params.getHeight() : 256, 8, 4096);
1035 if (w > 4096 || h > 4096) {
1036 error = "texture size too large (max 4096)";
1037 return bake;
1038 }
1039 const TileOpts opts = tileFromParams(params);
1040 NoiseField n;
1041 n.seed = params.getSeed();
1042 const bool seamless = params.getInt("seamless", 1) != 0;
1043 if (seamless) {
1044 n.periodX = std::max(1, opts.tilesX);
1045 n.periodY = std::max(1, opts.tilesY);
1046 }
1047
1048 bake.w = w;
1049 bake.h = h;
1050 bake.seamless = seamless;
1051 bake.height.assign(size_t(w) * size_t(h), 0.5f);
1052 bake.albedo = std::make_unique<image::ImageData>(w, h, "RGBA8");
1053 const int px = opts.pixelSize;
1054 for (int y = 0; y < h; ++y) {
1055 const int by = (y / px) * px;
1056 for (int x = 0; x < w; ++x) {
1057 const int bx = (x / px) * px;
1058 const float u = (float(bx) + 0.5f) / float(w);
1059 const float v = (float(by) + 0.5f) / float(h);
1060
1061 TileCell cell;
1062 if (opts.pattern == "diamond") {
1063 cell = sampleDiamond(u, v, opts.tilesX, opts.tilesY, opts.grout);
1064 } else if (opts.pattern == "hex") {
1065 cell = sampleHex(u, v, opts.tilesX, opts.tilesY, opts.grout);
1066 } else if (opts.pattern == "subway" || opts.pattern == "brick") {
1067 cell = sampleSubway(u, v, opts.tilesX, opts.tilesY, opts.grout);
1068 } else if (opts.pattern == "stack") {
1069 cell = sampleSquare(u, v, opts.tilesX, opts.tilesY, opts.grout);
1070 } else if (opts.pattern == "mosaic") {
1071 cell = sampleMosaic(u, v, opts.tilesX, opts.tilesY, opts.grout, n);
1072 } else if (opts.pattern == "basket" || opts.pattern == "basketweave") {
1073 cell = sampleBasketTile(u, v, opts.tilesX, opts.tilesY, opts.grout);
1074 } else if (opts.pattern == "herringbone") {
1075 const PlankCell p = sampleHerringbone(u, v, opts.tilesY, opts.tilesX, opts.grout);
1076 cell.idX = p.idX;
1077 cell.idY = p.idY;
1078 cell.localU = p.localU;
1079 cell.localV = p.localV;
1080 cell.edge = p.edge;
1081 cell.inGrout = p.inGap;
1082 cell.colorAlt = (p.idX + p.idY) & 1;
1083 } else if (opts.pattern == "octagon") {
1084 cell = sampleOctagon(u, v, opts.tilesX, opts.tilesY, opts.grout);
1085 } else if (opts.pattern == "fishscale" || opts.pattern == "scallop") {
1086 cell = sampleFishscale(u, v, opts.tilesX, opts.tilesY, opts.grout);
1087 } else if (opts.pattern == "pinwheel" || opts.pattern == "windmill") {
1088 cell = samplePinwheel(u, v, opts.tilesX, opts.tilesY, opts.grout);
1089 } else if (opts.pattern == "star" || opts.pattern == "moroccan") {
1090 cell = sampleStar(u, v, opts.tilesX, opts.tilesY, opts.grout);
1091 } else if (opts.pattern == "cobble") {
1092 cell = sampleCobble(u, v, opts.tilesX, opts.tilesY, opts.grout, n);
1093 } else if (opts.pattern == "arabesque") {
1094 cell = sampleArabesque(u, v, opts.tilesX, opts.tilesY, opts.grout);
1095 } else if (opts.pattern == "terrazzo") {
1096 cell = sampleSquare(u, v, opts.tilesX, opts.tilesY, opts.grout * 0.35f);
1097 } else {
1098 // square / checker / ceramic default
1099 cell = sampleSquare(u, v, opts.tilesX, opts.tilesY, opts.grout);
1100 }
1101
1102 Rgb col = shadeTile(opts, cell, n, u, v);
1103 if (opts.colors < 64) {
1104 const float bands = float(opts.colors);
1105 col.r = std::floor(col.r * bands) / bands;
1106 col.g = std::floor(col.g * bands) / bands;
1107 col.b = std::floor(col.b * bands) / bands;
1108 }
1109 writePixel(*bake.albedo, x, y, col);
1110 bake.height[size_t(y) * size_t(w) + size_t(x)] = tileDisplacement(opts, cell, n, u, v);
1111 }
1112 }
1113 return bake;
1114}
1115
1116std::unique_ptr<image::ImageData> makeTileImage(const Params& params, std::string& error) {
1117 FloorBake bake = bakeTileFloor(params, error);
1118 return std::move(bake.albedo);
1119}
1120
1121std::unique_ptr<image::ImageData> woodRecipe(const Params& params, std::string& error) {
1122 return makeWoodImage(params, error);
1123}
1124
1125std::unique_ptr<image::ImageData> tileRecipe(const Params& params, std::string& error) {
1126 return makeTileImage(params, error);
1127}
1128
1129RecipeDescriptor woodDescriptor() {
1130 RecipeDescriptor schema =
1131 RecipeDescriptor::grid("tex.floor.wood", "Wood Floor", "Floor", 8, 8, 4096, 4096);
1132 schema.params.push_back(ParamDescriptor::choice(
1133 "layout", "Layout", "planks",
1134 {"planks", "staggered", "herringbone", "chevron", "parquet", "basket", "diagonal", "ladder", "finger",
1135 "versailles"}));
1136 schema.params.push_back(ParamDescriptor::choice(
1137 "tone", "Wood Tone", "oak", {"oak", "walnut", "pine", "cherry", "ebony", "ash", "maple", "teak"}));
1138 schema.params.push_back(ParamDescriptor::integer("rows", "Plank Rows", 6, 1, 64));
1139 schema.params.push_back(ParamDescriptor::integer("cols", "Plank Columns", 4, 1, 64));
1140 schema.params.push_back(ParamDescriptor::floating("gap", "Groove Width", 0.04f, 0.f, 0.35f, 0.005f));
1141 schema.params.push_back(ParamDescriptor::floating("grain", "Grain Density", 8.f, 0.5f, 64.f, 0.1f));
1142 schema.params.push_back(ParamDescriptor::floating("warp", "Grain Warp", 1.4f, 0.f, 8.f, 0.05f));
1143 schema.params.push_back(ParamDescriptor::floating("wear", "Surface Wear", 0.25f, 0.f, 1.f, 0.01f));
1144 schema.params.push_back(ParamDescriptor::floating("stain", "Stain", 0.15f, 0.f, 1.f, 0.01f));
1145 schema.params.push_back(ParamDescriptor::floating("bevel", "Edge Bevel", 0.08f, 0.f, 0.4f, 0.01f));
1146 schema.params.push_back(ParamDescriptor::integer("colors", "Color Bands", 8, 2, 64));
1147 schema.params.push_back(ParamDescriptor::integer("pixelSize", "Pixel Size", 1, 1, 64));
1148 schema.params.push_back(ParamDescriptor::boolean("seamless", "Seamless", true));
1149 return schema;
1150}
1151
1152RecipeDescriptor tileDescriptor() {
1153 RecipeDescriptor schema =
1154 RecipeDescriptor::grid("tex.floor.tile", "Floor Tile", "Floor", 8, 8, 4096, 4096);
1155 schema.params.push_back(ParamDescriptor::choice(
1156 "pattern", "Pattern", "square",
1157 {"square", "checker", "diamond", "hex", "subway", "brick", "stack", "mosaic", "basket", "basketweave",
1158 "herringbone", "octagon", "fishscale", "scallop", "pinwheel", "windmill", "star", "moroccan", "cobble",
1159 "arabesque", "terrazzo"}));
1160 schema.params.push_back(ParamDescriptor::choice(
1161 "palette", "Palette", "ceramic",
1162 {"ceramic", "terracotta", "slate", "porcelain", "marble", "black", "mosaic", "subway", "encaustic", "jade",
1163 "cobalt"}));
1164 schema.params.push_back(ParamDescriptor::integer("tilesX", "Tiles X", 4, 1, 64));
1165 schema.params.push_back(ParamDescriptor::integer("tilesY", "Tiles Y", 4, 1, 64));
1166 schema.params.push_back(ParamDescriptor::floating("grout", "Grout Width", 0.06f, 0.f, 0.4f, 0.005f));
1167 schema.params.push_back(ParamDescriptor::floating("bevel", "Edge Bevel", 0.10f, 0.f, 0.45f, 0.01f));
1168 schema.params.push_back(ParamDescriptor::floating("glaze", "Glaze", 0.35f, 0.f, 1.f, 0.01f));
1169 schema.params.push_back(ParamDescriptor::floating("wear", "Wear", 0.20f, 0.f, 1.f, 0.01f));
1170 schema.params.push_back(ParamDescriptor::floating("speckles", "Speckles", 0.15f, 0.f, 1.f, 0.01f));
1171 schema.params.push_back(ParamDescriptor::floating("motif", "Motif Strength", 0.55f, 0.f, 1.f, 0.01f));
1172 schema.params.push_back(ParamDescriptor::integer("colors", "Color Bands", 8, 2, 64));
1173 schema.params.push_back(ParamDescriptor::integer("pixelSize", "Pixel Size", 1, 1, 64));
1174 schema.params.push_back(ParamDescriptor::boolean("seamless", "Seamless", true));
1175 return schema;
1176}
1177
1178} // namespace
1179
1181 std::string error;
1182 auto img = makeWoodImage(params, error);
1183 if (!img) {
1186 error.empty() ? "wood floor texture generation failed" : error, "recipe"));
1187 }
1188 return eve::Result<std::unique_ptr<image::ImageData>>::success(std::move(img));
1189}
1190
1192 std::string error;
1193 auto img = makeTileImage(params, error);
1194 if (!img) {
1197 error.empty() ? "tile floor texture generation failed" : error, "recipe"));
1198 }
1199 return eve::Result<std::unique_ptr<image::ImageData>>::success(std::move(img));
1200}
1201
1203 std::string error;
1204 FloorBake bake = bakeWoodFloor(params, error);
1205 auto set = assembleFloorPbr(std::move(bake), params, woodPbrDefaults(), error);
1206 if (!set) {
1209 error.empty() ? "wood floor PBR generation failed" : error, "recipe"));
1210 }
1211 return eve::Result<std::unique_ptr<PbrTextureSet>>::success(std::move(set));
1212}
1213
1215 std::string error;
1216 FloorBake bake = bakeTileFloor(params, error);
1217 FloorPbrDefaults defaults = tilePbrDefaults();
1218 // Glaze pushes the high-end roughness toward polish.
1219 const float glaze = std::clamp(params.getFloat("glaze", 0.35f), 0.f, 1.f);
1220 defaults.roughnessHigh = mixf(0.48f, 0.18f, glaze);
1221 auto set = assembleFloorPbr(std::move(bake), params, defaults, error);
1222 if (!set) {
1225 error.empty() ? "tile floor PBR generation failed" : error, "recipe"));
1226 }
1227 return eve::Result<std::unique_ptr<PbrTextureSet>>::success(std::move(set));
1228}
1229
1231 registry.registerRecipe(woodDescriptor(), woodRecipe);
1232 registry.registerRecipe(tileDescriptor(), tileRecipe);
1233}
1234
1236 RecipeDescriptor woodSchema = woodDescriptor();
1237 woodSchema.id = "pbr.floor.wood";
1238 woodSchema.displayName = "Wood Floor PBR";
1239 woodSchema.category = "Material";
1240 appendPbrParams(woodSchema, woodPbrDefaults());
1241 registry.registerPbrRecipe(std::move(woodSchema), [](const Params& params, std::string& error) {
1242 FloorBake bake = bakeWoodFloor(params, error);
1243 return assembleFloorPbr(std::move(bake), params, woodPbrDefaults(), error);
1244 });
1245
1246 RecipeDescriptor tileSchema = tileDescriptor();
1247 tileSchema.id = "pbr.floor.tile";
1248 tileSchema.displayName = "Floor Tile PBR";
1249 tileSchema.category = "Material";
1250 appendPbrParams(tileSchema, tilePbrDefaults());
1251 registry.registerPbrRecipe(std::move(tileSchema), [](const Params& params, std::string& error) {
1252 FloorBake bake = bakeTileFloor(params, error);
1253 FloorPbrDefaults defaults = tilePbrDefaults();
1254 const float glaze = std::clamp(params.getFloat("glaze", 0.35f), 0.f, 1.f);
1255 defaults.roughnessHigh = mixf(0.48f, 0.18f, glaze);
1256 return assembleFloorPbr(std::move(bake), params, defaults, error);
1257 });
1258}
1259
1260} // namespace eve::procgen
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
float roughnessLow
float aoStrength
float wear
bool seamless
float heightStrength
float gap
float normalStrength
float roughnessHigh
float cx
Definition CardTypes.cpp:33
float cy
Definition CardTypes.cpp:34
int ax
Definition CaveMesh.cpp:113
int ay
Definition CaveMesh.cpp:113
int bx
Definition CaveMesh.cpp:114
int by
Definition CaveMesh.cpp:114
float py
graphics::Texture * albedo
glm::vec4 p[6]
int rows
float bevel
float motif
float speckles
int colorAlt
int cols
float stain
float localU
float glaze
float warp
std::string pattern
float grain
bool inGap
int idX
bool inGrout
float localV
int idY
int tilesX
int pixelSize
float grout
std::string palette
std::string layout
std::string tone
int tilesY
tensor::Graph g
Definition GpuGraph.cpp:7
float u
Definition Grass.cpp:233
glm::vec3 n
Definition Grass.cpp:63
std::array< double, 10 > q
double r
float v
std::int32_t second
std::int32_t c
int h
std::vector< Colorf > px
std::uint32_t height
std::string local
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float metallic
std::string error
Definition Package.cpp:60
float d
float t
std::vector< float > colors
const RoadEdge * edge
float dy
float dx
Cell cell
std::map< Cell, int > best
float size
Definition TreeMesh.cpp:156
std::string body
double oy
double ox
float m[16]
float qx
float qz
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
medialoader::Colorf Colorf
Definition ImageData.h:41
Owning, typed generation parameters.
Definition Params.h:27
EVENGINE_API_DOMAINS public API.
Definition PbrMaterial.h:55
void registerPbrRecipe(const std::string &id, PbrRecipeFn fn)
Register a recipe without metadata.
EVENGINE_API_DOMAINS public API.
void registerRecipe(const std::string &id, TextureRecipeFn fn)
Register a recipe without metadata.
std::vector< ParamSpec > params
eve::Result< std::unique_ptr< PbrTextureSet > > generateTileFloorPbr(const Params &params)
Generate a full PBR map set for ceramic / patterned tiles (pbr.floor.tile). @ownership On success the...
void registerFloorPbrRecipes(PbrRecipeRegistry &registry)
Register pbr.floor.wood and pbr.floor.tile with schema metadata.
std::unique_ptr< image::ImageData > grayscaleImage(const std::vector< float > &values, int w, int h)
Builds a grayscale RGBA8 image and returns unique ownership to the caller.
eve::Result< std::unique_ptr< image::ImageData > > generateTileFloorTexture(const Params &params)
Generate a parameterized ceramic / patterned floor-tile albedo (tex.floor.tile). @ownership On succes...
void registerFloorTextureRecipes(TextureRecipeRegistry &registry)
Register tex.floor.wood and tex.floor.tile with schema metadata.
eve::Result< std::unique_ptr< image::ImageData > > generateWoodFloorTexture(const Params &params)
Generate a parameterized wood-floor albedo (tex.floor.wood). @ownership On success the caller exclusi...
eve::Result< std::unique_ptr< PbrTextureSet > > generateWoodFloorPbr(const Params &params)
Generate a full PBR map set for wood floors (pbr.floor.wood). @ownership On success the caller exclus...
std::unique_ptr< image::ImageData > heightToNormalImage(const std::vector< float > &height, int w, int h, float strength, bool seamless)
Builds a normal-map image and transfers its unique ownership to the caller.
WidgetDesc row(std::vector< WidgetDesc > children, std::string id)
Horizontal elastic layout row.
Definition Widget.cpp:679
static ParamDescriptor boolean(std::string key, std::string label, bool defaultValue)
Construct a Boolean descriptor stored as zero or one in Params.
static ParamDescriptor choice(std::string key, std::string label, std::string defaultValue, std::vector< std::string > choices)
Construct a finite-choice string descriptor.
static ParamDescriptor floating(std::string key, std::string label, float defaultValue, float minimum, float maximum, float step)
Construct a bounded floating-point descriptor.
static ParamDescriptor integer(std::string key, std::string label, int defaultValue, int minimum, int maximum, int step=1)
Construct a bounded integer descriptor.
Complete metadata and parameter schema shared by every procgen recipe family.
Definition ParamSchema.h:52
static RecipeDescriptor grid(std::string id, std::string displayName, std::string category, int minimumWidth, int minimumHeight, int maximumWidth=4096, int maximumHeight=4096)
Create a schema with common seed, width and height parameters.