A forest of octahedral impostors

Hi guys :grin:

I am working on a new library for rendering octahedral impostors and I’ve created a small demo with a terrain 3072 x 3072 and 200k trees. It should work quite good on mobile as well.

The terrain is managed with BatchedMesh with an extension to have LODs, generated with meshoptimizer.

For the trees, I used InstancedMesh2, with a BVH for frustum culling, adding two more LODs, one created with meshoptimizer (distance from 15 to 100) and the other using an octahedral impostor (distance 100+).

Demo: https://octahedral-impostor.vercel.app

Github : https://github.com/agargaro/octahedral-impostor

As soon as I find some time, I would like to share with you the details that allowed me to optimize it on mobile devices too.

21 Likes

Really cool stuff, I was working on the same thing a while back, some 3 years ago


Had some issues with interpolation, you can see lines on the sprite if you look closely. Probably lack of secondary reprojection.

At the time I was going full steam ahead on this,

  • added support for complex objects, not just a single mesh
  • variable atlas resolution
  • variable grid resolution so you can trade spatial for angular resolution
  • full octahedron and hemioct
  • debug visualization tools
  • margins to support mipmaps without bleeding
  • depth/normal/pbr + albedo/alpha bakes, so you can fully relight the thing and do proper depth compositing

Here is an example of a reasonable and unreasonable grid resolution on the same atlas resolution


Spent an ungodly amount of time on optimizing the bake.
Implemented Sprite fitting to reduce overdraw




Pick a number of points, say 4 - and the tool will fit a convex hull around it with that number of points. Lets you cut down on overdraw. Humus wrote about it over a decade ago, I thought it was a really cool idea.

I still want to bring this to production someday, maybe in Shade. Anyway, Kudos on making it work!

My references

3 Likes

For the most part this technique requires no dithering. If you do it right, the model will look exactly the same to the user with the impostor. You have to make sure to perform the substitution when the substituted model would occupy smaller or at least equal screen space than the bake resolution. The whole point of this technique is not to simplify the result, but to pre-bake it, so that the appearance matches the source exactly.

There can be popping due to insufficient angular resolution, or incorrect blending or undersampling (the point about resolution), but in general it’s not going to be required.

Problem with fading/dither is that when you do it - you’re drawing two things. Say you fade LODs, you’re drawing 2 LODs during that fade, so for a while - it’s worse than just the higher LOD.

Few engines do proper LOD fading, there are some high-profile ones that do, but in general most people don’t bother.

If you want to avoid popping - best way today is to use continuous LOD, and with things like impostors - if you use them carefully (see above) - you should see no popping at all.


As for my implementation - I didn’t even bother with fading. For @agargaro - I can’t really comment, I only had a brief look at his code but didn’t see anything like that.

1 Like

Hi @Usnul!

I’ve read almost all of your fantastic posts and I’m really happy to receive more information here on this post :grin: Thank you very much!

I would like to understand how to reduce the overdraw. Do I need to create a convex hull that contains all the sprites? Since the selection of the 3 sprites to be blended is done in the vertex shader, I don’t think it’s easy to create a convex hull every time just for the 3 selected sprites.

My library already has some customizations, but there is still work to be done before it is truly usable.

const impostor = new OctahedralImpostor({
  renderer,
  target: mesh/group,
  useHemiOctahedron: true,
  transparent: false,
  alphaClamp: 0.45,
  spritesPerSide: 16,
  textureSize: 2048,
  parallaxScale: 0.5,
  baseType: MeshLambertMaterial
});

scene.add(impostor);

@garry426milner No, I don’t think I’ll add it unless requested. @Usnul has already explained why.

1 Like

That’s a really good question. It’s what I thought about when I was working on too. Convex hull on a union of all sprites is what I went with. You can theoretically do better, but any given pixel on the screen can come from a different sprite, so hulls per-sprite don’t help a lot, as you’d have to do a lot of math later on.

One thought would be to use a 3d convex hull and project that, but that comes with a ton of other complications.

So yeah, for me - I just go with a union of all sprites. On average I had about 30% of pixels shaved off with just 5 vertices (3 triangles), but your results may vary.

That’s cool, love seeing others overengineer their prototypes :smiley:

1 Like