14constexpr float kPi = 3.14159265358979323846f;
17 float x = 0.f,
y = 0.f,
z = 0.f;
23float dot(V3
a, V3
b) {
return a.x *
b.x +
a.y *
b.y +
a.z *
b.z; }
25 return {
a.y *
b.z -
a.z *
b.y,
a.z *
b.x -
a.x *
b.z,
a.x *
b.y -
a.y *
b.x};
28 const float l = std::sqrt(
dot(
a,
a));
29 return l > 1e-8f ? V3{
a.x / l,
a.y / l,
a.z / l} : V3{0.f, 1.f, 0.f};
33void emitOpenTube(MeshBuild& out,
const std::vector<V3>&
path,
float radius,
int radialSegs,
float uvScale,
36 radialSegs = std::clamp(radialSegs, 3, 48);
37 const int rings = int(
path.size());
39 std::vector<V3> tangents = std::vector<V3>(
static_cast<size_t>(rings));
40 for (
int i = 0; i < rings; ++i) {
43 else if (i + 1 == rings)
50 std::vector<V3> frameN = std::vector<V3>(
static_cast<size_t>(rings));
51 std::vector<V3> frameB = std::vector<V3>(
static_cast<size_t>(rings));
52 V3 n0 = std::fabs(tangents[0].
y) < 0.9f ? V3{0, 1, 0} : V3{0, 0, 1};
53 frameN[0] =
normalize(n0 - tangents[0] *
dot(n0, tangents[0]));
54 frameB[0] =
cross(tangents[0], frameN[0]);
55 for (
int i = 1; i < rings; ++i) {
56 const V3
axis =
cross(tangents[
size_t(i - 1)], tangents[
size_t(i)]);
57 const float axisLen = std::sqrt(
dot(axis, axis));
58 if (axisLen < 1e-6f) {
59 frameN[size_t(i)] = frameN[size_t(i - 1)];
60 frameB[size_t(i)] = frameB[size_t(i - 1)];
62 const V3
a =
axis * (1.f / axisLen);
63 const float ang = std::atan2(axisLen,
dot(tangents[
size_t(i - 1)], tangents[
size_t(i)]));
64 const float ca = std::cos(
ang);
65 const float sa = std::sin(
ang);
66 auto rotate = [&](V3
v) {
69 frameN[size_t(i)] =
normalize(rotate(frameN[
size_t(i - 1)]));
70 frameB[size_t(i)] =
cross(tangents[
size_t(i)], frameN[
size_t(i)]);
74 std::vector<float> arc = std::vector<float>(
static_cast<size_t>(rings), 0.f);
75 for (
int i = 1; i < rings; ++i) {
76 const V3
d =
path[size_t(i)] -
path[size_t(i - 1)];
77 arc[size_t(i)] = arc[size_t(i - 1)] + std::sqrt(
dot(
d,
d));
80 const uint32_t base = uint32_t(out.getVertexCount());
81 for (
int i = 0; i < rings; ++i) {
82 for (
int j = 0; j <= radialSegs; ++j) {
83 const float a = 2.f * kPi * float(j) / float(radialSegs);
84 const float ca = std::cos(
a);
85 const float sa = std::sin(
a);
86 const V3
n = frameN[size_t(i)] * ca + frameB[size_t(i)] * sa;
88 const float u = (
a *
radius + uOffset) * uvScale;
89 const float v = arc[size_t(i)] * uvScale;
90 out.addVertex(
p.x,
p.y,
p.z,
n.x,
n.y,
n.z,
u,
v);
93 const int stride = radialSegs + 1;
94 for (
int i = 0; i + 1 < rings; ++i) {
95 for (
int j = 0; j < radialSegs; ++j) {
96 const uint32_t
a = base + uint32_t(i * stride + j);
97 const uint32_t
b =
a + uint32_t(stride);
98 out.addTriangle(
a,
b,
a + 1);
99 out.addTriangle(
a + 1,
b,
b + 1);
108void emitOvalLink(MeshBuild& out, V3
center,
float majorX,
float majorY,
float minorR,
int majorSegs,
int minorSegs,
109 bool vertical,
float uvScale) {
110 majorSegs = std::clamp(majorSegs, 8, 64);
111 minorSegs = std::clamp(minorSegs, 4, 24);
112 const uint32_t base = uint32_t(out.getVertexCount());
113 for (
int i = 0; i <= majorSegs; ++i) {
114 const float u = float(i) / float(majorSegs);
115 const float a = 2.f * kPi *
u;
116 const float ca = std::cos(
a);
117 const float sa = std::sin(
a);
119 V3 majorPos, majorT, planeN;
122 majorT =
normalize(V3{-majorX * sa, majorY * ca, 0.f});
123 planeN = {0.f, 0.f, 1.f};
126 majorT =
normalize(V3{-majorX * sa, 0.f, majorY * ca});
127 planeN = {0.f, 1.f, 0.f};
130 for (
int j = 0; j <= minorSegs; ++j) {
131 const float v = float(j) / float(minorSegs);
132 const float b = 2.f * kPi *
v;
133 const float cb = std::cos(
b);
134 const float sb = std::sin(
b);
135 const V3
n = radial * cb + planeN * sb;
136 const V3
p = majorPos +
n * minorR;
137 out.addVertex(
p.x,
p.y,
p.z,
n.x,
n.y,
n.z,
a * ((majorX + majorY) * 0.5f) * uvScale,
138 b * minorR * uvScale);
141 const int stride = minorSegs + 1;
142 for (
int i = 0; i < majorSegs; ++i) {
143 for (
int j = 0; j < minorSegs; ++j) {
144 const uint32_t
a = base + uint32_t(i * stride + j);
145 const uint32_t
b =
a + uint32_t(stride);
146 out.addTriangle(
a,
a + 1,
b);
147 out.addTriangle(
a + 1,
b + 1,
b);
152void buildTwistedStrands(MeshBuild& out,
int segments,
float L,
float radius,
float strandRadius,
int strands,
153 int twists,
int lengthSegs,
int radialSegs,
float uvScale,
bool hempBulge) {
155 twists = std::max(1, twists);
156 lengthSegs = std::clamp(lengthSegs, 4, 128);
157 radialSegs = std::clamp(radialSegs, 4, 24);
158 const float coreR = std::max(0.f,
radius - strandRadius * 1.15f);
162 for (
int seg = 0; seg < segments; ++seg) {
163 const float x0 = float(seg) * L;
164 std::vector<V3>
path;
165 path.reserve(
size_t(lengthSegs + 1));
166 for (
int i = 0; i <= lengthSegs; ++i) {
167 const float t = float(i) / float(lengthSegs);
168 const float x = x0 +
t * L;
170 const float a =
phase + 2.f * kPi * float(twists) *
t;
174 r *= 0.92f + 0.08f * std::sin(
a * 3.f +
float(
s));
176 path.push_back({
x,
r * std::cos(
a),
r * std::sin(
a)});
178 float sr = strandRadius;
179 if (hempBulge) sr *= 1.05f;
185void buildChain(MeshBuild& out,
int segments,
float L,
float radius,
float thickness,
int majorSegs,
int minorSegs,
189 const float pitch = L * 0.5f;
192 const float minorR = std::clamp(
thickness, 0.005f, majorY * 0.45f);
194 for (
int seg = 0; seg < segments; ++seg) {
195 const float baseX = float(seg) * L +
pitch * 0.5f;
197 emitOvalLink(out, {baseX, 0.f, 0.f}, majorX, majorY, minorR, majorSegs, minorSegs,
false, uvScale);
199 emitOvalLink(out, {baseX +
pitch, 0.f, 0.f}, majorX, majorY, minorR, majorSegs, minorSegs,
true, uvScale);
205 RecipeDescriptor
descriptor{std::move(
id), std::move(
name),
"Cable", {}};
226 const int segments = std::clamp(
params.getInt(
"segments", 6), 1, 256);
227 const float L = std::max(0.1f,
params.getFloat(
"segLength", 1.f));
228 const float uvScale = std::max(0.f,
params.getFloat(
"uvRepeat", 2.f));
229 const float scale = std::max(0.01f,
params.getFloat(
"scale", 1.f));
230 const int lengthSegs = std::clamp(
params.getInt(
"lengthSegs", 16), 4, 128);
231 const int radialSegs = std::clamp(
params.getInt(
"radialSegs", 8), 3, 48);
232 const int majorSegs = std::clamp(
params.getInt(
"majorSegs", 24), 8, 64);
233 const int minorSegs = std::clamp(
params.getInt(
"minorSegs", 8), 4, 24);
237 if (
kind ==
"mesh.cable") {
238 const float radius = std::max(0.01f,
params.getFloat(
"radius", 0.08f));
239 const float thickness = std::max(0.005f,
params.getFloat(
"thickness", 0.022f));
240 const int strands = std::clamp(
params.getInt(
"strands", 6), 2, 12);
241 const int twists = std::max(1,
params.getInt(
"twists", 1));
242 out.
reserve(segments *
strands * (lengthSegs + 1) * (radialSegs + 1),
243 segments *
strands * lengthSegs * radialSegs * 6);
244 buildTwistedStrands(out, segments, L,
radius,
thickness,
strands, twists, lengthSegs, radialSegs, uvScale,
246 }
else if (
kind ==
"mesh.rope") {
247 const float radius = std::max(0.01f,
params.getFloat(
"radius", 0.10f));
248 const float thickness = std::max(0.005f,
params.getFloat(
"thickness", 0.045f));
249 const int strands = std::clamp(
params.getInt(
"strands", 3), 2, 12);
250 const int twists = std::max(1,
params.getInt(
"twists", 1));
251 out.
reserve(segments *
strands * (lengthSegs + 1) * (radialSegs + 1),
252 segments *
strands * lengthSegs * radialSegs * 6);
253 buildTwistedStrands(out, segments, L,
radius,
thickness,
strands, twists, lengthSegs, radialSegs, uvScale,
255 }
else if (
kind ==
"mesh.chain") {
256 const float radius = std::max(0.01f,
params.getFloat(
"radius", 0.12f));
257 const float thickness = std::max(0.005f,
params.getFloat(
"thickness", 0.035f));
258 out.
reserve(segments * 2 * (majorSegs + 1) * (minorSegs + 1),
259 segments * 2 * majorSegs * minorSegs * 6);
260 buildChain(out, segments, L,
radius,
thickness, majorSegs, minorSegs, uvScale);
262 error =
"unknown cable/chain/rope kind '" +
kind +
"' (use mesh.cable|mesh.chain|mesh.rope)";
270 error =
kind +
": empty mesh (check segments/radius/thickness)";
274 out.
setMeta(
"segments", std::to_string(segments));
279 registry.
registerRecipe(makeSchema(
"mesh.cable",
"Steel Cable", 0.08f, 0.022f, 6, 1),
283 registry.
registerRecipe(makeSchema(
"mesh.chain",
"Iron Chain", 0.12f, 0.035f, 2, 1),
287 registry.
registerRecipe(makeSchema(
"mesh.rope",
"Hemp Rope", 0.10f, 0.045f, 3, 1),
std::array< float, 3 > scale
CPU triangle mesh from procedural mesh recipes (e.g. marching cubes). Positions/normals are xyz-packe...
const std::vector< float > & positions() const
Positions.
void reserve(int vertexCount, int indexCount)
Reserve.
void setMeta(const std::string &key, const std::string &value)
Sets the meta.
EVENGINE_API_DOMAINS public API.
void registerRecipe(const std::string &id, MeshRecipeFn fn)
Register a recipe without metadata.
Owning, typed generation parameters.
std::vector< ParamSpec > params
Vec3 operator-(Vec3 lhs, Vec3 rhs)
Operator -.
Vec3 operator+(Vec3 lhs, Vec3 rhs)
Operator +.
Vec3 operator*(Vec3 value, float scale)
Operator *.
Vec2 normalize(const Vec2 &a)
Normalize.
double dot(const Vec2 &a, const Vec2 &b)
Dot.
double cross(const Vec2 &a, const Vec2 &b)
Cross.
void registerCableChainRopeRecipes(MeshRecipeRegistry ®istry)
Register mesh.cable / mesh.chain / mesh.rope mesh recipes.
bool generateCableChainRope(const std::string &kind, const Params ¶ms, MeshBuild &out, std::string &error)
Procedural tileable cable / chain / rope meshes along +X.
int axis(int64_t a, size_t rank)
Axis.
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.