fenomas / noa

Experimental voxel game engine.
MIT License
611 stars 87 forks source link

Consider using SoildParticleSystem for terrain chunks #119

Closed Jarred-Sumner closed 2 years ago

Jarred-Sumner commented 4 years ago

SolidParticleSystem for large scenes seems to reduce GPU frame time & draw calls considerably.

I changed some of terrainMesher to use a SolidParticleSystem when there are multiple materials:

     const sps = new SolidParticleSystem("sps", scene, {
        updatable: false,
        isPickable: true,
        useModelMaterial: true,
        particleIntersection: true,
        enableMultiMaterial: mesh.material instanceof MultiMaterial,
      });

      sps.addShape(mesh, 1);
      mesh.dispose();

      return sps.buildMesh();
Before After
fenomas commented 4 years ago

This is really interesting, thanks! Do you have any idea what the reason is? I have a strong feeling that there's some relatively simple "right thing to do", that SPS is doing and noa isn't, and if we do that then we'll get the benefits without actually invoking SPS (which presumably has some overhead).

At a glance, presumably it looks like each chunk of terrain (which should be one mesh with a multimaterial and N submeshes) is taking N draw calls in noa and 1 call after SPS. I'm looking around the SPS source for a reason but nothing is jumping out so far..

terrac commented 4 years ago

It looks like the SPS is just a single updating mesh. So that could be the reason.

fenomas commented 4 years ago

IIRC noa is the same, it should be making each chunk into one mesh with a multimaterial. SPS looks like the same deal?

Jarred-Sumner commented 4 years ago

At a glance, presumably it looks like each chunk of terrain (which should be one mesh with a multimaterial and N submeshes) is taking N draw calls in noa and 1 call after SPS. I'm looking around the SPS source for a reason but nothing is jumping out so far..

Okay I think I understand now why this happens.

Two reasons:

  1. I recently added a texture atlas. I was reading this article and this passage stood out to me:

    One naive solution might be to create a separate texture for each block type, and then do a separate pass for each of these textures. However, this would require a number of state changes proportional to O(number of chunks * number of textures). In a world with hundreds of textures and thousands of chunks, this would be utterly unacceptable from a performance standpoint. Instead, a better solution is to use a technique called texture atlases.

The texture atlas allows me to use a single material and texture for all terrain blocks. I just change the UV offsets appropriately for each block. This won't work if you have thousands of textures (rather than hundreds), but in that case, you can have multiple texture atlases and there still should be significant performance gains.

  1. This paragraph from the docs on Solid Particle System:

    In order to have only one draw call to the GPU, these three systems use only one material/texture for all their particles.

So basically, a regular Mesh with several SubMesh seems to result in mulitple draw calls, even with a shared material/texture. Whereas a SolidParticleSystem will be O(unique material) draw calls?

What's not clear to me is why Mesh doesn't work like SolidParticleSystem this way

fenomas commented 4 years ago

Hi, okay, that explains things. I'm familiar with that article but it says that there should be significant rendering artifacts when using a texture atlas for terrain, unless you do the various stuff it describes, which AFAICT would require a custom shader to do in Babylon. Is that not what you're seeing?

Jarred-Sumner commented 4 years ago

TLDR: This is complicated and maybe impractical to generally support

Truthfully, there've been rendering artifacts from day one for me (https://github.com/andyhall/noa/issues/108)

I wrote a custom shader just now, mostly copy pasted from SimpleMaterial. I didn't think it was necessary to do that when I first opened this issue

That blog post was written before WebGL2 – WebGL2's sampler2DArray gives you array textures so you can avoid needing most of the tricks the author wrote about. In Babylon.js, this is a RawTexture2DArray

The downside is...WebGL 2 is not supported on Safari.

The code for initializing the texture looks like this:

const atlasTexture = new RawTexture2DArray(
      await createImageBitmap(img),
      TILE_WIDTH,
      IMAGE_HEIGHT,
      TILE_COUNT,
      Engine.TEXTUREFORMAT_RGBA,
      scene,
      false,
      true,
      Texture.NEAREST_SAMPLINGMODE
    );

To make the image work, instead of being a square, its just a really tall image. One column

Most of the only differences really between the default SimpleMaterial and mine is:

Fragment Shader:

-uniform sampler2D diffuseSampler;
+uniform sampler2DArray diffuseSampler;

+varying float vTile;
-baseColor = texture2D(diffuseSampler, vDiffuseUV);
+baseColor.rgb = texture(diffuseSampler, vec3(vDiffuseUV, vTile)).rgb;

Vertex Shader:

+attribute float tile;
+varying float vTile;
+vTile = tile;

Then, when building the mesh, you pass it a FloatArray of the block IDs that is 4 per tile duplicated (so one dirt block is 1,1,1,1):

newMesh.setVerticesData("tile", submesh.tiles, false, 1);

This is what it looks like. There's a blue-ish hue but there's some environment color I need to pass through it that I'm missing demo 1

I still need to do the complicated things he describes in the article to support Safari though...

fenomas commented 4 years ago

Hey, this looks really interesting. Do you have it in a branch somewhere I could try out?

Personally, I'm pretty okay with not supporting Safari if there's a solid performance reason. Also they seem to have WebGL2 support behind a flag, so it's probably coming sooner or later.

Jarred-Sumner commented 4 years ago

I put noa in a directory of the codebase for the game itself and made some other modifications to it (removing most usages of anonymous functions which the profiler showed it reduced memory usage. Also I got meshing to work in a worker but it wasn't faster because remeshing happens often). I'd honestly be fine with sharing repo access or sending you a link to try it. The game itself is not open source though.

fenomas commented 2 years ago

It took me a while, but since Babylon now (sort of) has a way to use sampler2D without fully authoring your own shader, I have hacked this out -- the newest engine build in #develop now supports texture atlases for terrain.

Basically all you do is:

noa.registry.registerMaterial('grass', {
    textureURL: 'terrain_atlas.png', 
    atlasIndex: 0,
})

The texture image should be a "vertical strip" atlas - i.e. a texture N pixels wide and N*M pixels tall, like this, and atlasIndex is a 0-based index for which tile of the atlas to use. Sample code can be found in the #develop branch of the example repo.

If anyone has a chance to test this, please let me know if you have issues!

fenomas commented 2 years ago

I'll close this since texture atlases seem to be working, but anyone feel free to comment if you have issues.