fenomas / noa

Experimental voxel game engine.
MIT License
608 stars 86 forks source link

Using a texture atlas for block materials #173

Closed nornagon closed 1 year ago

nornagon commented 2 years ago

Hey there! I'm working off the noa-examples 'hello-world' example, trying to load in some Minecraft-style terrain textures. Minecraft stores its texture image as a 256x256 .png image, and I'd like to load it as-is and use subsets of it to texture blocks.

Of course, just specifying the atlas as a texture results in the whole image being applied to the side of each block.

I'm trying to make a Babylon Material that uses different UV coordinates, but something in the terrain mesher seems to be defeating me:

Screen Shot 2022-01-27 at 8 00 09 PM

Here's my material:

const baseTexture = new Texture('terrain.png', noa.rendering.getScene(), true, false, Texture.NEAREST_SAMPLINGMODE)
const dirtTexture = baseTexture.clone()
dirtTexture.uScale = 16 / 256
dirtTexture.vScale = 16 / 256
dirtTexture.uOffset = 2 * 16 / 256
dirtTexture.vOffset = 0 * 16 / 256
dirtTexture.wrapU = Texture.WRAP_ADDRESSMODE
dirtTexture.wrapV = Texture.WRAP_ADDRESSMODE // These appear to have no effect
const dirtMat = noa.rendering.makeStandardMaterial('')
dirtMat.diffuseTexture = dirtTexture
noa.registry.registerMaterial('dirt', null, null, false, dirtMat)

Any thoughts as to what I might do to get this functioning as a texture atlas?

Thanks!

fenomas commented 2 years ago

Hi! In an ideal world noa would already be doing this, but it would require a custom shader (and someone to write/maintain it who knows more about shaders than I do).

Basically, Babylon's standard shader supports uv offset and scaling, but as you noticed they can't be combined with wrapping - basically you can use a texture atlas, or you can repeat a texture, but not both at once. In a custom shader both can be done, but there are other complications - the whole topic is discussed in depth here.

nornagon commented 2 years ago

Huh, that article suggests that using texture arrays would make the problem "easy", but that WebGL doesn't support them. I think that has changed since the article was written? WebGL 2 seems to support texture arrays, e.g. https://github.com/WebGLSamples/WebGL2Samples/blob/master/samples/texture_2d_array.html

fenomas commented 2 years ago

Oooh, interesting... it looks like Babylon has support for this too. Let me look into this more.

fenomas commented 1 year ago

Hi, I forgot to reply here but as noted in a different issue I have hacked out support for terrain to use texture atlases in the #develop branch.

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.

Please let me know if you have issues!

MCArth commented 1 year ago

Hi @fenomas, I imagine #develop is one draw call per chunk now? Which sounds great! Have you done any testing to see what the performance improvements are like?

fenomas commented 1 year ago

I imagine #develop is one draw call per chunk now?

@MCArth Yes - try it out! Strictly, it's one draw call per chunk per material, and block faces that use the same texture atlas will share a material. But solid-color blocks use a separate material, as do solid colors with transparency.

I also added a "stress test" world to the examples repo, which draws a large-ish world with crunch terrain features, and with all terrain blocks from a texture atlas, and the performance seems to be good. But I'm not 100% sure that it's limited on draw calls (or was before texture atlas support).

FYI I have other meshing changes that aren't pushed yet, which speed up meshing quite a bit . (The speed of generating terrain meshes that is, not of rendering them afterwards).

MCArth commented 1 year ago

Just done some testing; seems much better. I used the stress test demo and modified it to pick one of the five blocks randomly (to represent an avg of ~5 draw calls per chunk), rendering took 4-5ms on stress test. I applied the same worldgen using master and it took 12-14ms so a nice improvement (of which much was the draw call related gl bindbuffer etc calls/related work).

Somewhat weirdly, in the stress test on develop I changed from texture atlas to registering separate images like this:

noa.registry.registerMaterial('grass', {textureURL: 'a.png'})
noa.registry.registerMaterial('g_dirt', {textureURL: 'b.png'})
noa.registry.registerMaterial('dirt', {textureURL: 'c.png'})
noa.registry.registerMaterial('stone', {textureURL: 'stone.png'})
noa.registry.registerMaterial('stone2', {textureURL: 't1.png'})
noa.registry.registerMaterial('cloud', {textureURL: 't2.png'})

and it took ~8ms per render tick so there's something else going on improving performance in develop. I took all measurements after all chunks have loaded/meshed - can you think of anything? Perhaps the newer babylon versions have improved perf under the hood

I'll wait a while till I bring this into my own fork (forking was just the way my development went early on...) - it'd be great if you could ping me when you think you've finished making changes, since all the merging takes a while

Thanks for all your work on noa :)

fenomas commented 1 year ago

@MCArth I just pushed updates to noa and noa-examples. The noa changes basically speed up meshing by about 3-4x, you should really notice the difference. The main change in examples is that I'm trying out esbuild for building, though of course webpack still works.

I can't promise they're the last changes for this version, but I have nothing else planned ;)

Out of curiosity, is anything in particular preventing you from abandoning your fork? I'm trying to keep the external API consistent enough, but also leave internals exposed, so that customizations can be done by importing the library and changing what you want at runtime. But if you're forking and merging each change, I guess that's going to be painful no matter what I do.

Thanks for testing!

MCArth commented 1 year ago

A lot of my changes have been pretty hacky, in order to move fast (and there's a lot of them, 118 commits, over the past 18 months). Porting it all back into my own code base would be an enormous amount of effort I can't justify.

I can offer some suggestions that may make it less likely newcomers to noa go down the same road I have though:

fenomas commented 1 year ago

Hi, thanks for the feedback. Yes, the idea behind having so many core features built as components is to make it easy to remove and override things. E.g. to replace the built-in movement logic:

    var myMoveComponent = {
        name: 'my-movement-component',
        state: { /* ... */ },
        system: (dt, states) => {
            for (var state of states) { /* ... */ }
        },
    }
    ents.createComponent(myMoveComponent)
    ents.removeComponent(noa.playerEntity, ents.names.movement)
    ents.addComponent(noa.playerEntity, myMoveComponent.name)

I suppose what's probably needed is something like a cookbook or wiki that shows some common things most game clients will need to do... or alternately one of the example worlds could be modified to show something like the previous.

Incidentally if you're wondering, custom camera logic can be done similarly - there's an entity noa.camera.cameraTarget, which the rendering system points the camera towards each render. That target entity moves around with the player because by default is has the follows-entity component, but you can remove that component and move it around by whatever other logic.

MCArth commented 1 year ago

It is indeed flexible, but I didn't properly investigate when I was first starting out :D

For getting me to do that, removing and re-adding a slightly modified built in component (e.g. movement) would have been the best bet

fenomas commented 1 year ago

Closing this as the feature seems to work, if anyone finds bugs file a new issue please!