SimonDev

SimonDev

@simondev.bsky.social

1K followers28 following168 posts

View full thread

One last little trick: Normal fog blending kinda sucks, it basically just blends towards a colour. Instead, split that into 2 separate calculations, one for extinction and one for in-scattering. • extinction = darker silhouettes • in-scattering = brighter haze

View full thread

The terrain looks decent, but it's lacking a sense of depth. Basic distance fog is easy: blend toward a blurred sample of the sky as the terrain gets farther away. It works, but it doesn't look awesome.

This is a cheap height fog from Crytek's 2006 presentation. Instead of looking only at distance, it integrates an exponentially thinning fog layer along the camera ray. Valleys fill with dense fog, while the peaks stay relatively clear.

View full thread

Terraces are another cheap shaping trick. Split the height range into steps, but keep a short ramp between each plateau. That gives us broad, flat areas without making every transition a vertical cliff.

Obviously, you can combine these however you want. Here we used low-frequency terraces to create the large shapes, then added the weathered fBM on top.

View full thread

A neat trick is to use noise derivatives, which tell you how steep the terrain is. Use that slope to reduce the smaller details in steep areas, and regular fBM starts to look weathered. Quick cheapie erosion effect.

View full thread

There are other types of noise. Voronoi noise scatters a point inside each grid cell, then calculates the distance to the nearest one. Checking the surrounding 3×3 cells gives us a completely different noise function.

fBM isn’t a special kind of noise. It’s just a way of layering noise. Here, we're layering the "ridged" noise from earlier. Notice this gives a much rockier effect.

View full thread

One layer of noise is smooth, but you need more to get convincing terrain. You can use fBM (fractional brownian motion). Sample the noise repeatedly, at different scales, adding more detail iteratively. Extra: pow() reshapes the height range. I used "3" to flatten valleys and make peaks stand out.

An easy approach to procedural terrain: heightmaps Start with a tesselated plane, and calculate the height for each vertex. In our example, every vertex samples cheap value noise: height = noise(position.xy * frequency) * amplitude; Frequency -> xy scaling of noise Amplitude -> terrain height

Noise is used everywhere in games: terrain, animations, camera shake, and more. But it can still feel like magic. How do you turn greyscale blobs into real effects? Here are a few practical ways to use it. #threejs #shaders

View full thread

For camera shake, use vector noise: offset = vectorNoise(time); This is just noise that gives you a 2D "random" value back. You can get the same thing by doing: offset.x = noise(time, seed1); offset.y = noise(time, seed2); This gets you smooth but unpredictable movement, with no jarring jumps.

Noise is used everywhere in games: terrain, animations, camera shake, and more. But it can still feel like magic. How do you turn greyscale blobs into real effects? Here are a few practical ways to use it. #threejs #shaders

View full thread

fBM noise is an amazing starting point for procedural terrain. Treat the noise as a heightmap, where brightness = elevation. Here, we're simply taking a grid and using fBM to choose the height at any given point.

Practical example: making a light flicker. Instead of random flicker, use 1D noise over time: • smooth variation • no harsh jumps • still feels organic light_intensity = noise(currentTime * frequency) * magnitude + bias; or light_intensity = fbmNoise(currentTime * frequency) * magnitude + bias;

Noise is used everywhere in games: terrain, animations, camera shake, and more. But it can still feel like magic. How do you turn greyscale blobs into real effects? Here are a few practical ways to use it. #threejs #shaders

View full thread

There are lots of different flavours of "basic" noise. Value, Gradient, Perlin, Simplex. They all look a bit different, but the general idea is: use them as a source of controlled variation.

One powerful trick is called fBM, short fractional (or fractal) Brownian motion. Basically layering noise at increasing frequencies and decreasing amplitudes. You sample the noise, transform the coordinates, and sample again. Over and over, blending the results together.

Noise is used everywhere in games: terrain, animations, camera shake, and more. But it can still feel like magic. How do you turn greyscale blobs into real effects? Here are a few practical ways to use it. #threejs #shaders

Most noise starts with a super simple trick: Give it some input, like a position (x, y), and it returns a repeatable pseudo-random value. The hash function internally does a bunch of math to spit out something that "looks random enough".

If you lay those random values out in space (a grid is the easiest) then you can interpolate between the points. This turns a bunch of random values into smooth "randomness", or noise.

SimonDev reposted

Here are some techniques I discovered through 14 years of shader programming:

Optimization can be tricky. Here’s how to go from drawing a few hundred trees to virtually unlimited in Three.js, step by step. This will be high level, but not so much that you can’t fill in the details. #threejs

View full thread

Once you've made it through all these steps • Reuse materials • Batch/instance • Optimize data • Cull • LOD/imposters We're hitting 1 million+ trees, for very little CPU/GPU cost.

Optimization can be tricky. Here’s how to go from drawing a few hundred trees to virtually unlimited in Three.js, step by step. This will be high level, but not so much that you can’t fill in the details. #threejs

View full thread

At this point, the last lever left is reducing quality. LOD (level-of-detail) works by dropping detail with distance. Further away objects, you swap meshes (LOD0 -> LOD1 -> LOD2) and nobody notices (hopefully) With instancing, you'll have to do this manually with an InstancedMesh for each level.

Octahedral imposters are a powerful technique, where we render the object from many angles into an atlas texture, then just show a billboard in the world. Key detail: it responds to camera movement and lighting, but it's just smoke and mirrors. TSL makes it easy to hook into the lighting system.

Optimization can be tricky. Here’s how to go from drawing a few hundred trees to virtually unlimited in Three.js, step by step. This will be high level, but not so much that you can’t fill in the details. #threejs

View full thread

You can quantize way further than most people think. It’s possible to squeeze a ~56B vertex down to ~16B with packing + quantization. TSL makes unpacking clean (override attributes via node API). Source : x.com/SebAaltonen/... This gets us to 50k+ trees.

At some point it’s hard to “draw faster”. So stop drawing stuff you can’t see. Frustum culling removes anything offscreen. Not automatic with InstancedMesh, so you can either: • Instance within a chunk, then cull by chunk. • Cull manually per-instance We’re at 250k+ trees now.

Optimization can be tricky. Here’s how to go from drawing a few hundred trees to virtually unlimited in Three.js, step by step. This will be high level, but not so much that you can’t fill in the details. #threejs

View full thread

Instancing allows us to tell the GPU in a single draw call: "hey, draw this thing a zillion times". No need for the CPU to constantly submit draw commands, which alleviates the load on the CPU and shifts the bottleneck to the GPU. We're hitting 30k+ trees now.

Optimization can be tricky. Here’s how to go from drawing a few hundred trees to virtually unlimited in Three.js, step by step. This will be high level, but not so much that you can’t fill in the details. #threejs

View full thread

Low-hanging fruit: stop duplicating assets. Share geometry & materials across instances and you'll see immediate improvements in the framerate. This change alone gets us to ~700–800 trees at 60fps.

To draw a lot of stuff, you want to reduce materials. Collapse different materials into a single material by packing textures into atlases. Then you can collapse draw calls with: • InstancedMesh (same geo) • BatchedMesh (different geo)

Optimization can be tricky. Here’s how to go from drawing a few hundred trees to virtually unlimited in Three.js, step by step. This will be high level, but not so much that you can’t fill in the details. #threejs

The first step is to make sure you’re measuring the right things. You need both CPU time and GPU time, to understand where the problems lie. I use three-perf or the Three.js Inspector so I can see both numbers easily.