19constexpr float kPi = 3.14159265358979323846f;
21float smoothstep(
float edge0,
float edge1,
float x) {
22 if (edge0 == edge1)
return x < edge0 ? 0.f : 1.f;
23 const float t = std::clamp((
x - edge0) / (edge1 - edge0), 0.f, 1.f);
24 return t *
t * (3.f - 2.f *
t);
27float mixf(
float a,
float b,
float t) {
28 t = std::clamp(
t, 0.f, 1.f);
29 return a + (
b -
a) *
t;
33 float r = 0.f,
g = 0.f,
b = 0.f;
36Rgb mix(Rgb
a, Rgb
b,
float t) {
37 t = std::clamp(
t, 0.f, 1.f);
38 return {
a.r + (
b.r -
a.r) *
t,
a.g + (
b.g -
a.g) *
t,
a.b + (
b.b -
a.b) *
t};
41void writePixel(image::ImageData& img,
int x,
int y, Rgb
c) {
46 std::unique_ptr<image::ImageData>
albedo;
53struct CablePbrDefaults {
62void appendPbrParams(RecipeDescriptor& schema,
const CablePbrDefaults&
d) {
63 schema.params.push_back(
65 schema.params.push_back(
68 schema.params.push_back(
70 schema.params.push_back(
72 schema.params.push_back(
76std::unique_ptr<PbrTextureSet> assembleCablePbr(CableBake bake,
const Params&
params,
const CablePbrDefaults& defaults,
78 if (!bake.albedo || bake.height.size() !=
size_t(bake.w) *
size_t(bake.h)) {
79 error =
"cable bake missing albedo or height";
86 std::clamp(
params.getFloat(
"roughnessLow", defaults.roughnessLow), 0.f, 1.f);
88 std::clamp(
params.getFloat(
"roughnessHigh", defaults.roughnessHigh), 0.f, 1.f);
89 const float metallic = std::clamp(
params.getFloat(
"metallic", defaults.metallic), 0.f, 1.f);
91 std::max(0.01f,
params.getFloat(
"normalStrength", defaults.normalStrength));
92 const float aoStrength = std::clamp(
params.getFloat(
"aoStrength", defaults.aoStrength), 0.f, 5.f);
94 std::max(0.f,
params.getFloat(
"heightStrength", defaults.heightStrength));
96 auto set = std::make_unique<PbrTextureSet>();
97 set->albedo = bake.albedo.release();
100 error =
"cable normal generation failed";
104 std::vector<float> rough(
size_t(
w *
h));
105 std::vector<float> heightMap(
size_t(
w *
h));
106 std::vector<float> ao(
size_t(
w *
h));
107 const int r = std::max(1, std::min(
w,
h) / 64);
108 for (
int y = 0;
y <
h; ++
y) {
109 for (
int x = 0;
x <
w; ++
x) {
110 const size_t i = size_t(
y) * size_t(
w) + size_t(
x);
111 const float hh = bake.height[i];
114 heightMap[i] = std::clamp(0.5f + (hh - 0.5f) *
heightStrength, 0.f, 1.f);
120 int xx =
x +
dx, yy =
y +
dy;
122 xx = ((xx %
w) +
w) %
w;
123 yy = ((yy %
h) +
h) %
h;
125 if (xx < 0 || yy < 0 || xx >=
w || yy >=
h)
continue;
127 sum += bake.height[size_t(yy) * size_t(
w) + size_t(xx)];
131 const float local = cnt > 0 ? sum / float(cnt) : hh;
132 ao[i] = std::clamp(1.f -
aoStrength * std::max(0.f,
local - hh), 0.f, 1.f);
139 if (!set->roughness || !set->metallic || !set->height || !set->ao) {
140 error =
"cable PBR map allocation failed";
156CommonOpts commonFromParams(
const Params&
params,
int defaultStrands,
float defaultTwist) {
158 o.strands = std::clamp(
params.getInt(
"strands", defaultStrands), 2, 16);
159 o.twist = std::clamp(
params.getFloat(
"twist", defaultTwist), 0.25f, 16.f);
160 o.gap = std::clamp(
params.getFloat(
"gap", 0.12f), 0.f, 0.45f);
161 o.wear = std::clamp(
params.getFloat(
"wear", 0.25f), 0.f, 1.f);
162 o.contrast = std::clamp(
params.getFloat(
"contrast", 0.55f), 0.f, 1.f);
163 o.colors = std::max(2,
params.getInt(
"colors", 8));
164 o.seamless =
params.getInt(
"seamless", 1) != 0;
169void strandField(
float u,
float v,
const CommonOpts& o,
float&
height,
float& strandId) {
170 const float twisted =
u * float(o.strands) +
v * o.twist;
171 const float cell = twisted - std::floor(twisted);
172 const float dist = std::min(
cell, 1.f -
cell) * 2.f;
173 const float ridge = smoothstep(o.gap, 1.f, dist);
175 strandId = std::floor(twisted);
178CableBake bakeSteelCable(
const Params&
params, std::string&
error) {
179 const int w = std::clamp(
params.getWidth() > 0 ?
params.getWidth() : 256, 8, 4096);
180 const int h = std::clamp(
params.getHeight() > 0 ?
params.getHeight() : 256, 8, 4096);
181 CommonOpts o = commonFromParams(
params, 6, 3.f);
182 const float polish = std::clamp(
params.getFloat(
"polish", 0.45f), 0.f, 1.f);
184 noise.seed = uint32_t(
params.getSeed());
185 noise.periodX = o.seamless ? 8 : 0;
186 noise.periodY = o.seamless ? 8 : 0;
191 bake.seamless = o.seamless;
192 bake.albedo = std::make_unique<image::ImageData>(
w,
h,
"RGBA8");
193 bake.height.assign(
size_t(
w *
h), 0.f);
195 error =
"steel cable albedo allocation failed";
199 const Rgb bright{0.72f, 0.74f, 0.76f};
200 const Rgb
mid{0.48f, 0.50f, 0.52f};
201 const Rgb dark{0.28f, 0.30f, 0.32f};
203 for (
int y = 0;
y <
h; ++
y) {
204 for (
int x = 0;
x <
w; ++
x) {
205 const float u = (float(
x) + 0.5f) /
float(
w);
206 const float v = (float(
y) + 0.5f) /
float(
h);
207 float hh = 0.f, sid = 0.f;
208 strandField(
u,
v, o, hh, sid);
210 noise.fbmPerlin(
u * 6.f + sid * 0.17f,
v * 4.f, 3, 2.f, 0.5f);
211 const float scratch = noise.ridgedPerlin(
u * 18.f,
v * 2.f + sid, 2, 2.2f, 0.55f);
212 float shade = mixf(0.35f, 1.f, hh);
213 shade = mixf(shade, shade * (0.85f + 0.3f *
n), o.contrast);
214 shade = mixf(shade, shade * (1.f - 0.35f * scratch), o.wear);
215 shade = mixf(shade, mixf(shade, 1.f, polish * 0.25f), polish);
216 Rgb col = mix(dark, mix(
mid, bright, shade), shade);
218 const float bands = float(o.colors);
219 col.r = std::floor(col.r * bands) / bands;
220 col.g = std::floor(col.g * bands) / bands;
221 col.b = std::floor(col.b * bands) / bands;
223 writePixel(*bake.albedo,
x,
y, col);
224 bake.height[size_t(
y) * size_t(
w) + size_t(
x)] =
225 std::clamp(hh * 0.85f +
n * 0.08f - scratch * 0.05f * o.wear, 0.f, 1.f);
231CableBake bakeIronChain(
const Params&
params, std::string&
error) {
232 const int w = std::clamp(
params.getWidth() > 0 ?
params.getWidth() : 256, 8, 4096);
233 const int h = std::clamp(
params.getHeight() > 0 ?
params.getHeight() : 256, 8, 4096);
234 CommonOpts o = commonFromParams(
params, 4, 1.5f);
235 const float rust = std::clamp(
params.getFloat(
"rust", 0.35f), 0.f, 1.f);
237 noise.seed = uint32_t(
params.getSeed());
238 noise.periodX = o.seamless ? 8 : 0;
239 noise.periodY = o.seamless ? 8 : 0;
244 bake.seamless = o.seamless;
245 bake.albedo = std::make_unique<image::ImageData>(
w,
h,
"RGBA8");
246 bake.height.assign(
size_t(
w *
h), 0.f);
248 error =
"iron chain albedo allocation failed";
252 const Rgb steel{0.42f, 0.44f, 0.46f};
253 const Rgb dark{0.18f, 0.17f, 0.16f};
254 const Rgb rustA{0.55f, 0.28f, 0.12f};
255 const Rgb rustB{0.38f, 0.18f, 0.08f};
257 for (
int y = 0;
y <
h; ++
y) {
258 for (
int x = 0;
x <
w; ++
x) {
259 const float u = (float(
x) + 0.5f) /
float(
w);
260 const float v = (float(
y) + 0.5f) /
float(
h);
262 const float band = 0.5f + 0.5f * std::sin(
u * 2.f * kPi *
float(o.strands) +
v * o.twist * kPi);
263 const float n = noise.fbmPerlin(
u * 5.f,
v * 5.f, 4, 2.f, 0.5f);
264 const float pit = smoothstep(0.72f, 0.92f, noise.ridgedPerlin(
u * 14.f,
v * 14.f, 2, 2.f, 0.5f));
265 const float rustMask =
266 smoothstep(0.55f, 0.9f, noise.fbm(
u * 3.f,
v * 3.f, 3, 2.f, 0.55f)) * rust;
267 float hh = mixf(0.35f, 0.85f, band) * (1.f - pit * 0.55f);
268 hh = mixf(hh, hh * 0.7f, rustMask);
269 Rgb col = mix(dark, steel, mixf(0.4f, 1.f, band) * (0.75f + 0.25f *
n));
270 col = mix(col, mix(rustB, rustA,
n), rustMask);
271 col = mix(col, dark, pit * 0.65f);
272 col.r = mixf(col.r, col.r * (1.f - 0.2f * o.wear), o.wear);
273 col.g = mixf(col.g, col.g * (1.f - 0.2f * o.wear), o.wear);
274 col.b = mixf(col.b, col.b * (1.f - 0.15f * o.wear), o.wear);
276 const float bands = float(o.colors);
277 col.r = std::floor(col.r * bands) / bands;
278 col.g = std::floor(col.g * bands) / bands;
279 col.b = std::floor(col.b * bands) / bands;
281 writePixel(*bake.albedo,
x,
y, col);
282 bake.height[size_t(
y) * size_t(
w) + size_t(
x)] = std::clamp(hh, 0.f, 1.f);
288CableBake bakeHempRope(
const Params&
params, std::string&
error) {
289 const int w = std::clamp(
params.getWidth() > 0 ?
params.getWidth() : 256, 8, 4096);
290 const int h = std::clamp(
params.getHeight() > 0 ?
params.getHeight() : 256, 8, 4096);
291 CommonOpts o = commonFromParams(
params, 3, 2.5f);
292 const float fiber = std::clamp(
params.getFloat(
"fiber", 0.65f), 0.f, 1.f);
294 noise.seed = uint32_t(
params.getSeed());
295 noise.periodX = o.seamless ? 8 : 0;
296 noise.periodY = o.seamless ? 8 : 0;
301 bake.seamless = o.seamless;
302 bake.albedo = std::make_unique<image::ImageData>(
w,
h,
"RGBA8");
303 bake.height.assign(
size_t(
w *
h), 0.f);
305 error =
"hemp rope albedo allocation failed";
309 const Rgb
light{0.72f, 0.58f, 0.34f};
310 const Rgb
mid{0.52f, 0.40f, 0.22f};
311 const Rgb dark{0.32f, 0.24f, 0.12f};
313 for (
int y = 0;
y <
h; ++
y) {
314 for (
int x = 0;
x <
w; ++
x) {
315 const float u = (float(
x) + 0.5f) /
float(
w);
316 const float v = (float(
y) + 0.5f) /
float(
h);
317 float hh = 0.f, sid = 0.f;
318 strandField(
u,
v, o, hh, sid);
320 noise.fbmPerlin(
u * 10.f + sid,
v * 18.f, 4, 2.1f, 0.5f);
321 const float fluff = noise.valueNoise(
u * 32.f,
v * 40.f);
322 float shade = mixf(0.25f, 1.f, hh);
323 shade = mixf(shade, shade * (0.7f + 0.5f *
n), fiber);
324 shade = mixf(shade, shade * (0.85f + 0.3f * fluff), o.wear * 0.5f + 0.2f);
325 Rgb col = mix(dark, mix(
mid,
light, shade), shade);
327 if (fluff > 0.78f) col = mix(col, dark, 0.35f * fiber);
329 const float bands = float(o.colors);
330 col.r = std::floor(col.r * bands) / bands;
331 col.g = std::floor(col.g * bands) / bands;
332 col.b = std::floor(col.b * bands) / bands;
334 writePixel(*bake.albedo,
x,
y, col);
335 bake.height[size_t(
y) * size_t(
w) + size_t(
x)] =
336 std::clamp(hh * 0.8f +
n * 0.15f * fiber + fluff * 0.05f, 0.f, 1.f);
342std::unique_ptr<image::ImageData> steelRecipe(
const Params&
params, std::string&
error) {
345std::unique_ptr<image::ImageData> ironRecipe(
const Params&
params, std::string&
error) {
348std::unique_ptr<image::ImageData> hempRecipe(
const Params&
params, std::string&
error) {
352void appendCommonParams(RecipeDescriptor& schema,
int strands,
float twist) {
362RecipeDescriptor steelDescriptor() {
363 RecipeDescriptor schema =
365 appendCommonParams(schema, 6, 3.f);
370RecipeDescriptor ironDescriptor() {
371 RecipeDescriptor schema =
373 appendCommonParams(schema, 4, 1.5f);
378RecipeDescriptor hempDescriptor() {
379 RecipeDescriptor schema =
381 appendCommonParams(schema, 3, 2.5f);
386CablePbrDefaults steelPbrDefaults() {
return {0.42f, 0.22f, 0.92f, 6.5f, 1.15f, 1.1f}; }
387CablePbrDefaults ironPbrDefaults() {
return {0.55f, 0.30f, 0.78f, 5.5f, 1.35f, 1.05f}; }
388CablePbrDefaults hempPbrDefaults() {
return {0.82f, 0.55f, 0.02f, 4.5f, 1.2f, 1.2f}; }
398 error.empty() ?
"steel cable texture generation failed" :
error,
"recipe"));
409 error.empty() ?
"iron chain texture generation failed" :
error,
"recipe"));
420 error.empty() ?
"hemp rope texture generation failed" :
error,
"recipe"));
428 auto set = assembleCablePbr(std::move(bake),
params, steelPbrDefaults(),
error);
432 error.empty() ?
"steel cable PBR generation failed" :
error,
"recipe"));
440 auto set = assembleCablePbr(std::move(bake),
params, ironPbrDefaults(),
error);
444 error.empty() ?
"iron chain PBR generation failed" :
error,
"recipe"));
452 auto set = assembleCablePbr(std::move(bake),
params, hempPbrDefaults(),
error);
456 error.empty() ?
"hemp rope PBR generation failed" :
error,
"recipe"));
469 steel.
id =
"pbr.cable.steel";
472 appendPbrParams(steel, steelPbrDefaults());
475 return assembleCablePbr(std::move(bake),
params, steelPbrDefaults(),
error);
479 iron.
id =
"pbr.chain.iron";
482 appendPbrParams(iron, ironPbrDefaults());
485 return assembleCablePbr(std::move(bake),
params, ironPbrDefaults(),
error);
489 hemp.
id =
"pbr.rope.hemp";
492 appendPbrParams(hemp, hempPbrDefaults());
495 return assembleCablePbr(std::move(bake),
params, hempPbrDefaults(),
error);
graphics::Texture * albedo
std::vector< float > colors
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
void registerCableTextureRecipes(TextureRecipeRegistry ®istry)
Register tex.cable.steel / tex.chain.iron / tex.rope.hemp.
eve::Result< std::unique_ptr< image::ImageData > > generateIronChainTexture(const Params ¶ms)
Generate a parameterized iron / rusted metal albedo (tex.chain.iron). @ownership On success the calle...
void registerCablePbrRecipes(PbrRecipeRegistry ®istry)
Register pbr.cable.steel / pbr.chain.iron / pbr.rope.hemp.
eve::Result< std::unique_ptr< image::ImageData > > generateSteelCableTexture(const Params ¶ms)
Generate a parameterized steel-cable braid albedo (tex.cable.steel). @ownership On success the caller...
eve::Result< std::unique_ptr< PbrTextureSet > > generateHempRopePbr(const Params ¶ms)
Full PBR map set for hemp rope (pbr.rope.hemp). @ownership On success the caller exclusively owns the...
eve::Result< std::unique_ptr< PbrTextureSet > > generateIronChainPbr(const Params ¶ms)
Full PBR map set for iron chain metal (pbr.chain.iron). @ownership On success the caller exclusively ...
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< PbrTextureSet > > generateSteelCablePbr(const Params ¶ms)
Full PBR map set for steel cable (pbr.cable.steel). @ownership On success the caller exclusively owns...
eve::Result< std::unique_ptr< image::ImageData > > generateHempRopeTexture(const Params ¶ms)
Generate a parameterized hemp-rope fiber albedo (tex.rope.hemp). @ownership On success the caller exc...
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.
static ParamDescriptor boolean(std::string key, std::string label, bool defaultValue)
Construct a Boolean descriptor stored as zero or one in Params.
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.