BloomCode & Turtlex
Hey BloomCode, I've been trying to build a GLSL‑based procedural plant generator and keep getting stuck on how to make the leaf curvature look natural. Got any tips or favorite techniques?
Hey, I totally get how tricky leaf curvature can be. A good trick is to start with a simple flat shape—think a rectangular strip—and then warp it along a spline. In GLSL you can use a cubic Bezier or just a simple quadratic curve to lift the center of the leaf.
Once you have that basic curve, add a little 2‑D noise to the offset so the leaf edge isn’t perfectly smooth. For example, use `vec2 noisePos = vec2(u_time, v_uv.y); float lift = noise(noisePos)*0.1;` and add `lift` to the y‑coordinate of the geometry. This gives a subtle waviness that feels natural.
Another tip: make the tip taper by blending the curvature with the distance to the leaf tip. Something like `float tipFactor = smoothstep(0.8, 1.0, v_uv.y); lift *= (1.0 - tipFactor);` so the leaf narrows and flattens out toward the tip.
If you want more organic detail, sample a 3‑D simplex noise field and displace the vertices along the normal. Keep the displacement small—maybe 0.02 units—so you don’t get a “mossy” look, but just enough to break the flatness.
Lastly, remember to keep your code modular. Write a small function that returns the displacement for a given UV, and call it from your vertex shader. That way you can tweak the curve, noise amplitude, or tapering independently without rewriting everything.
Hope that helps! Let me know if you hit any snags.
Sounds solid, thanks. I’ll start with the Bezier warp and then layer the simplex noise as you suggested. Do you have a snippet for that noise function, or should I just use the classic GLSL hash? Also, any tricks for keeping the tip’s angle consistent across different leaf widths?
Sure thing! Here’s a tiny, self‑contained 2‑D simplex‑style hash you can paste into your shader. It’s fast enough for a leaf generator and gives you a decent noise field:
```glsl
// 2‑D hash – returns a pseudo‑random value in [0,1]
float hash(vec2 p){
p = fract(p*0.3183099 + 0.1);
p *= 17.0;
return fract(p.x*p.y*(p.x+p.y));
}
// 2‑D noise – smooths the hash with a lerp
float noise(vec2 p){
vec2 i = floor(p);
vec2 f = fract(p);
// Cubic smoothstep
vec2 u = f*f*(3.0-2.0*f);
float n00 = hash(i+vec2(0.0,0.0));
float n10 = hash(i+vec2(1.0,0.0));
float n01 = hash(i+vec2(0.0,1.0));
float n11 = hash(i+vec2(1.0,1.0));
float nx0 = mix(n00, n10, u.x);
float nx1 = mix(n01, n11, u.x);
return mix(nx0, nx1, u.y);
}
```
Use it like this for the leaf warp:
```glsl
vec2 uv = v_uv; // 0..1 across leaf
// Bezier curve along y (height)
vec2 p0 = vec2(0.0,0.0);
vec2 p1 = vec2(0.0,0.5);
vec2 p2 = vec2(0.0,1.0);
float t = uv.y;
float lift = (1.0 - pow(1.0-t,3.0)); // simple cubic lift
lift += noise(vec2(uv.x*10.0, uv.y*10.0))*0.05; // subtle noise
vec3 pos = vec3(uv.x, uv.y + lift, 0.0);
```
---
**Keeping the tip angle consistent**
The trick is to make the tip angle a function of relative distance, not absolute width. Compute a “height factor” that’s 1 at the base and tapers to 0 at the tip, then blend the angle with that:
```glsl
float heightFactor = smoothstep(0.0, 0.9, uv.y); // 1 at base, near 0 at tip
float baseAngle = radians(15.0); // how sharp the leaf is at the base
float tipAngle = radians(5.0); // gentle angle at the tip
float curAngle = mix(baseAngle, tipAngle, heightFactor);
vec2 dir = normalize(vec2(sin(curAngle), cos(curAngle)));
vec3 displaced = vec3(uv.x, uv.y, 0.0) + dir * lift; // lift from noise
```
Because `curAngle` interpolates with `heightFactor`, the angle stays proportional no matter how wide the leaf’s base is. That keeps the overall look natural across different sizes.
Give it a go and tweak the constants until it feels right for your style! Happy coding and growing 🌱