noa icon indicating copy to clipboard operation
noa copied to clipboard

Using a texture atlas for block materials

Open nornagon opened this issue 4 years ago • 3 comments

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!

nornagon avatar Jan 28 '22 04:01 nornagon

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.

fenomas avatar Jan 28 '22 04:01 fenomas

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

nornagon avatar Jan 28 '22 05:01 nornagon

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

fenomas avatar Jan 28 '22 05:01 fenomas

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!

fenomas avatar Aug 21 '22 03:08 fenomas

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?

MCArth avatar Aug 21 '22 09:08 MCArth

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).

fenomas avatar Aug 22 '22 08:08 fenomas

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 :)

MCArth avatar Aug 23 '22 11:08 MCArth

@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!

fenomas avatar Aug 24 '22 16:08 fenomas

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:

  • Iirc one of the first changes I made inside noa was to the movement component. Providing an example of removing one of the built in components, customising it and re-adding it inside of one of the noa-examples might go a long way.
  • This could of course gone in userspace but I defined objects on noa since it was passed anywhere already. A babylon-esque userData object could be good
  • When I made camera changes for a third person shooting perspective - see pic - it didn't seem like it would be straightforward to do the same outside noa. I see you've more recently made some changes there though which might have made it easier Iirc more hooks in the render tick would have been needed? E.g. emits before and after entities.render (and perhaps it would have been possible all along, I didn't look into it too much). image

MCArth avatar Aug 31 '22 09:08 MCArth

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.

fenomas avatar Sep 01 '22 04:09 fenomas

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

MCArth avatar Sep 01 '22 09:09 MCArth

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

fenomas avatar Sep 16 '22 04:09 fenomas