Closed Jarred-Sumner closed 2 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..
It looks like the SPS is just a single updating mesh. So that could be the reason.
IIRC noa is the same, it should be making each chunk into one mesh with a multimaterial. SPS looks like the same deal?
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:
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.
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
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?
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
I still need to do the complicated things he describes in the article to support Safari though...
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.
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.
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!
I'll close this since texture atlases seem to be working, but anyone feel free to comment if you have issues.
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: