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);
27 float r = 0.f,
g = 0.f,
b = 0.f;
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};
35float mixf(
float a,
float b,
float t) {
36 t = std::clamp(
t, 0.f, 1.f);
37 return a + (
b -
a) *
t;
42 std::unique_ptr<image::ImageData>
albedo;
49struct FloorPbrDefaults {
58void appendPbrParams(RecipeDescriptor& schema,
const FloorPbrDefaults&
d) {
59 schema.params.push_back(
61 schema.params.push_back(
64 schema.params.push_back(
66 schema.params.push_back(
68 schema.params.push_back(
72FloorPbrDefaults woodPbrDefaults() {
74 return {0.70f, 0.38f, 0.02f, 6.f, 1.25f, 1.15f};
77FloorPbrDefaults tilePbrDefaults() {
79 return {0.78f, 0.28f, 0.0f, 5.5f, 1.4f, 1.05f};
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";
92 std::clamp(
params.getFloat(
"roughnessLow", defaults.roughnessLow), 0.f, 1.f);
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);
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);
100 std::max(0.f,
params.getFloat(
"heightStrength", defaults.heightStrength));
102 auto set = std::make_unique<PbrTextureSet>();
103 set->albedo = bake.albedo.release();
106 error =
"floor normal generation failed";
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];
119 heightMap[i] = std::clamp(0.5f + (hh - 0.5f) *
heightStrength, 0.f, 1.f);
125 int xx =
x +
dx, yy =
y +
dy;
127 xx = ((xx %
w) +
w) %
w;
128 yy = ((yy %
h) +
h) %
h;
130 if (xx < 0 || yy < 0 || xx >=
w || yy >=
h)
continue;
132 sum += bake.height[size_t(yy) * size_t(
w) + size_t(xx)];
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);
144 if (!set->roughness || !set->metallic || !set->height || !set->ao) {
145 error =
"floor PBR map allocation failed";
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};
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};
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};
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};
201void writePixel(image::ImageData& img,
int x,
int y, Rgb
c) {
220WoodOpts woodFromParams(
const Params&
params) {
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));
252TileOpts tileFromParams(
const Params&
params) {
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));
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;
286PlankCell samplePlanks(
float u,
float v,
int rows,
int cols,
float gap,
float stagger) {
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);
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);
307PlankCell sampleHerringbone(
float u,
float v,
int rows,
int cols,
float gap) {
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);
322 const float edge = std::min(std::min(fu, 1.f - fu), std::min(fv, 1.f - fv));
328PlankCell sampleParquet(
float u,
float v,
int rows,
int cols,
float gap) {
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;
341 const int strip = int(std::floor(pu));
342 const float su = pu - float(strip);
344 cell.idX = bxI * 16 + strip;
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);
352 const float blockEdge = std::min(std::min(fx, 1.f - fx), std::min(fy, 1.f - fy));
354 cell.edge =
cell.inGap ? 0.f : smoothstep(
gap,
gap + 0.08f, std::min(
edge, blockEdge));
358PlankCell sampleDiagonal(
float u,
float v,
int rows,
int cols,
float gap) {
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);
371 const float edge = std::min(std::min(fu, 1.f - fu), std::min(fv, 1.f - fv));
377PlankCell sampleLadder(
float u,
float v,
int rows,
int cols,
float gap) {
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);
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);
398PlankCell sampleVersailles(
float u,
float v,
int rows,
int cols,
float gap) {
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);
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;
412 cell.idX = ix * 8 + (onArm ? 1 : 0) + (onFrame ? 2 : 0);
415 cell.localU = frame / 0.10f;
416 cell.localV = (std::fabs(fx - 0.5f) < std::fabs(fy - 0.5f)) ? fy : fx;
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;
423 const bool horiz = ((fx < 0.5f) == (fy < 0.5f));
424 cell.localU = horiz ? fx : fy;
425 cell.localV = horiz ? fy : fx;
428 const float edge = onFrame ? frame : (onArm ? arm : std::min(frame, arm));
434Rgb shadeWood(
const WoodOpts& opts,
const PlankCell&
cell,
float grain,
const NoiseField&
n,
float u,
float v) {
435 Rgb base = toneToRgb(opts.tone);
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);
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);
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));
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);
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;
464float woodDisplacement(
const WoodOpts& opts,
const PlankCell&
cell,
float grain,
const NoiseField&
n,
float u,
467 const float grit =
n.valueNoise(
u * 36.f,
v * 36.f);
468 return 0.08f + grit * 0.05f;
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))) *
475 return mixf(0.20f, 0.62f + micro - scratch, 0.28f + 0.72f * bevelAmt);
478FloorBake bakeWoodFloor(
const Params&
params, std::string&
error) {
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)";
486 const WoodOpts opts = woodFromParams(
params);
491 n.periodX = std::max(1, opts.cols);
492 n.periodY = std::max(1, opts.rows);
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) {
503 for (
int x = 0;
x <
w; ++
x) {
505 const float u = (float(
bx) + 0.5f) /
float(
w);
506 const float v = (float(
by) + 0.5f) /
float(
h);
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;
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;
540 woodGrain(grainU * opts.grain * 0.35f, grainV * opts.grain * 0.12f,
n, opts.grain * 0.55f, opts.warp);
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;
550 writePixel(*bake.albedo,
x,
y, col);
551 bake.height[size_t(
y) * size_t(
w) + size_t(
x)] =
558std::unique_ptr<image::ImageData> makeWoodImage(
const Params&
params, std::string&
error) {
560 return std::move(bake.albedo);
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));
583 c.edge =
c.inGrout ? 0.f : smoothstep(
grout,
grout + 0.08f, e);
584 c.colorAlt = (
c.idX +
c.idY) & 1;
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;
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));
600 c.edge =
c.inGrout ? 0.f : smoothstep(
grout,
grout + 0.08f, e);
601 c.colorAlt = (
c.idX +
c.idY) & 1;
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;
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)
617 else if (yDiff > zDiff)
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;
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));
648 c.edge =
c.inGrout ? 0.f : smoothstep(
grout,
grout + 0.08f, e);
649 c.colorAlt =
c.idX & 1;
653TileCell sampleMosaic(
float u,
float v,
int tilesX,
int tilesY,
float grout,
const NoiseField&
n) {
657 const int cx0 = int(std::floor(
sx));
658 const int cy0 = int(std::floor(
sy));
660 int bx = cx0,
by = cy0;
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);
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);
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;
706 const float strip = fy * 2.f;
707 c.idY = iy * 2 + int(std::floor(strip));
710 c.localV = strip - std::floor(strip);
712 const float strip = fx * 2.f;
713 c.idX = ix * 2 + int(std::floor(strip));
715 c.localU = strip - std::floor(strip);
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));
722 c.edge =
c.inGrout ? 0.f : smoothstep(
grout,
grout + 0.08f, std::min(e, blockEdge));
723 c.colorAlt = (
c.idX +
c.idY) & 1;
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);
736 const float manhattan = std::fabs(fx - 0.5f) + std::fabs(fy - 0.5f);
737 const bool insert = manhattan > 0.68f;
741 const int cx = ix + (fx > 0.5f ? 1 : 0);
742 const int cy = iy + (fy > 0.5f ? 1 : 0);
745 c.localU = std::fmod(fx + 0.5f, 1.f);
746 c.localV = std::fmod(fy + 0.5f, 1.f);
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);
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);
761 c.edge =
c.inGrout ? 0.f : smoothstep(
grout,
grout + 0.08f, e);
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));
776 for (
int oy = -1;
oy <= 1; ++
oy) {
777 for (
int ox = -1;
ox <= 1; ++
ox) {
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);
796 c.localU = 0.5f + (
sx - (float(
bx) + 0.5f - ((
by & 1) ? 0.5f : 0.f)));
798 c.colorAlt = (
bx +
by) & 1;
800 c.edge =
c.inGrout ? 0.f : smoothstep(0.52f -
grout * 0.35f - 0.08f, 0.52f -
grout * 0.35f,
best);
802 if (!
c.inGrout)
c.edge = 1.f -
c.edge;
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);
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;
823 const float e = std::min(std::min(
c.localU, 1.f -
c.localU), std::min(
c.localV, 1.f -
c.localV));
825 c.edge =
c.inGrout ? 0.f : smoothstep(
grout,
grout + 0.08f, e);
830 if (fy < fx && fy < 1.f - fx)
832 else if (fy >= fx && fy < 1.f - fx)
834 else if (fy >= 1.f - fx && fy >= fx)
839 c.colorAlt = 1 + (blade & 1);
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);
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);
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);
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);
875TileCell sampleCobble(
float u,
float v,
int tilesX,
int tilesY,
float grout,
const NoiseField&
n) {
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);
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);
896 const int row = int(std::floor(
sy));
897 const float off = (
row & 1) ? 0.5f : 0.f;
900 for (
int oy = -1;
oy <= 1; ++
oy) {
901 for (
int ox = -1;
ox <= 1; ++
ox) {
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;
909 const float d = std::sqrt(
dx *
dx +
dy *
dy);
922 c.colorAlt = (
bx +
by) & 1;
924 const float ring = std::fabs(std::sin(
best * 6.28318f * 1.5f));
926 c.edge =
c.inGrout ? 0.f : smoothstep(0.f, 0.1f, 0.48f -
best) * (0.4f + 0.6f * ring);
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);
935 if (opts.pattern ==
"checker" || opts.pattern ==
"pinwheel" || opts.pattern ==
"windmill") {
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);
940 }
else if (opts.pattern ==
"star" || opts.pattern ==
"octagon") {
941 body = (
cell.colorAlt == 0) ?
a : mix(
a,
b, 0.65f);
943 const float tileVar =
n.hash01(
cell.idX + 11,
cell.idY + 19) * 0.12f - 0.06f;
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));
955 if (!
cell.inGrout && opts.motif > 0.05f) {
956 const float dx =
cell.localU - 0.5f;
957 const float dy =
cell.localV - 0.5f;
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;
967 m = smoothstep(0.15f, 0.55f,
m) * opts.motif;
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);
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));
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);
985 const float speck =
n.valueNoise(
u * 22.f +
float(
cell.idX),
v * 22.f +
float(
cell.idY));
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);
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);
995 Rgb
grout = groutRgb(opts.palette);
997 const float grit =
n.valueNoise(
u * 40.f,
v * 40.f);
1002 const float bevelAmt = smoothstep(0.f, opts.bevel + 1e-4f,
cell.edge);
1003 return mix(
grout,
body, 0.35f + 0.65f * bevelAmt);
1006float tileDisplacement(
const TileOpts& opts,
const TileCell&
cell,
const NoiseField&
n,
float u,
float v) {
1008 const float grit =
n.valueNoise(
u * 40.f,
v * 40.f);
1009 return 0.10f + grit * 0.06f;
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;
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;
1026 if (
m > 0.15f &&
m < 0.55f) hh -= opts.motif * 0.04f;
1028 return std::clamp(hh, 0.f, 1.f);
1031FloorBake bakeTileFloor(
const Params&
params, std::string&
error) {
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)";
1039 const TileOpts opts = tileFromParams(
params);
1044 n.periodX = std::max(1, opts.tilesX);
1045 n.periodY = std::max(1, opts.tilesY);
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) {
1056 for (
int x = 0;
x <
w; ++
x) {
1058 const float u = (float(
bx) + 0.5f) /
float(
w);
1059 const float v = (float(
by) + 0.5f) /
float(
h);
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);
1078 cell.localU =
p.localU;
1079 cell.localV =
p.localV;
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);
1099 cell = sampleSquare(
u,
v, opts.tilesX, opts.tilesY, opts.grout);
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;
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);
1116std::unique_ptr<image::ImageData> makeTileImage(
const Params&
params, std::string&
error) {
1118 return std::move(bake.albedo);
1121std::unique_ptr<image::ImageData> woodRecipe(
const Params&
params, std::string&
error) {
1125std::unique_ptr<image::ImageData> tileRecipe(
const Params&
params, std::string&
error) {
1129RecipeDescriptor woodDescriptor() {
1130 RecipeDescriptor schema =
1133 "layout",
"Layout",
"planks",
1134 {
"planks",
"staggered",
"herringbone",
"chevron",
"parquet",
"basket",
"diagonal",
"ladder",
"finger",
1137 "tone",
"Wood Tone",
"oak", {
"oak",
"walnut",
"pine",
"cherry",
"ebony",
"ash",
"maple",
"teak"}));
1152RecipeDescriptor tileDescriptor() {
1153 RecipeDescriptor schema =
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"}));
1161 "palette",
"Palette",
"ceramic",
1162 {
"ceramic",
"terracotta",
"slate",
"porcelain",
"marble",
"black",
"mosaic",
"subway",
"encaustic",
"jade",
1186 error.empty() ?
"wood floor texture generation failed" :
error,
"recipe"));
1197 error.empty() ?
"tile floor texture generation failed" :
error,
"recipe"));
1205 auto set = assembleFloorPbr(std::move(bake),
params, woodPbrDefaults(),
error);
1209 error.empty() ?
"wood floor PBR generation failed" :
error,
"recipe"));
1217 FloorPbrDefaults defaults = tilePbrDefaults();
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);
1225 error.empty() ?
"tile floor PBR generation failed" :
error,
"recipe"));
1237 woodSchema.
id =
"pbr.floor.wood";
1240 appendPbrParams(woodSchema, woodPbrDefaults());
1243 return assembleFloorPbr(std::move(bake),
params, woodPbrDefaults(),
error);
1247 tileSchema.
id =
"pbr.floor.tile";
1250 appendPbrParams(tileSchema, tilePbrDefaults());
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);
graphics::Texture * albedo
std::array< double, 10 > q
std::vector< float > colors
std::map< Cell, int > best
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.
Move-only operation result carrying either a value or Status.
medialoader::Colorf Colorf
Owning, typed generation parameters.
EVENGINE_API_DOMAINS public API.
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 ¶ms)
Generate a full PBR map set for ceramic / patterned tiles (pbr.floor.tile). @ownership On success the...
void registerFloorPbrRecipes(PbrRecipeRegistry ®istry)
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 ¶ms)
Generate a parameterized ceramic / patterned floor-tile albedo (tex.floor.tile). @ownership On succes...
void registerFloorTextureRecipes(TextureRecipeRegistry ®istry)
Register tex.floor.wood and tex.floor.tile with schema metadata.
eve::Result< std::unique_ptr< image::ImageData > > generateWoodFloorTexture(const Params ¶ms)
Generate a parameterized wood-floor albedo (tex.floor.wood). @ownership On success the caller exclusi...
eve::Result< std::unique_ptr< PbrTextureSet > > generateWoodFloorPbr(const Params ¶ms)
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.
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.
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.