nfagerlund / bevy-tablestakes

takin a new thing for a spin
0 stars 0 forks source link

Draw shadows, handle off-the-ground height for sprite-guys #5

Closed nfagerlund closed 1 year ago

nfagerlund commented 1 year ago

Right, so in the shaunjs thing, we put this in the object's draw event, which procedurally laid down the shadow sprite first and then did a sprite draw call with the Y offset by Z.

Here, we don't have that type of control over the draw behavior per-object. There's also outstanding questions of whether that's best dealt with via custom draw logic, or via the entity hierarchy and the components already being used by the 2d drawing pipeline.

(An example of option 2, in theory: make the shadow a child of the player entity, set its transform Z to -0.1 so its sprite goes behind... and then how to deal with the altitude offset? Well, you could fuck with the player sprite's anchor point -- some screwy arithmetic there because it's relative to the sprite box instead of in absolute units. Or, you could maintain an entirely separate translation component for the simulated 3D space and make sure to only read/write that when moving people around, then sync it to the real Translation that the render world extracts at the last minute with the Y offset by the simulated Z. I don't love this!)

As for option 1, I'm still in the process of figuring out what the options even are. Material shader...???

nfagerlund commented 1 year ago

Here's what I wrote on the topic in a post on the bevy help channel/forum, which no one responded to yet:

In a top-down Zelda-like setup with 2D graphics, you often want a fake 3D effect: objects have an in-simulation Z (height) value, and then their sprites get rendered with their Y position offset by their Z. (And their shadow gets rendered non-offset, so it looks like it's cast onto the ground.)

What's the best way to accomplish that with Bevy? The options I can think of so far are:

  • Stop using Transform for in-simulation positioning, and make a copy of it to use instead. Sync the simulation transform to the stock Transform in a late-running system before the render stages start, and offset the Y at that point.

  • Use Transform like normal, and track simulated Z in a separate Height component. A new system that runs in the Extract stage after extract_sprites can find all the entities with Heights, and offset the transform Ys in the render world versions of them.

  • Same as the last one, but use Transform's Z to track simulated height and a separate Depth component to track sprite layering for render order. An Extract system uses Z to offset Y in the render world, then replaces Z with the depth. (Hmm, I probably need to do something like this for depth sorting anyway...)

(By the way, in all of the above I'm assuming the shadow sprite should probably be a child entity of the game object that casts it, with its depth sorted behind.)

  • Handle the offset in the main world by dinking with the sprite's Anchor::Custom value. (This might not be viable, and I definitely hate it; the fact that Anchor::Custom is a proportion rather than a pixel value makes this real unergonomic, and I think the approach would go off the rails for entities that have "attachment" child sprites that need to move along with their parents.)

  • Some other kind of custom rendering logic? (2D Mesh instead of sprites? Shaders??) I'm baby and don't actually know what this would entail or how practical it would be with the way Bevy's 2D pipeline is set up.

Thanks for reading!

nfagerlund commented 1 year ago

And I think I like my third idea the best:

Same as the last one, but use Transform's Z to track simulated height and a separate Depth component to track sprite layering for render order. An Extract system uses Z to offset Y in the render world, then replaces Z with the depth. (Hmm, I probably need to do something like this for depth sorting anyway...)

I like 1. the elegance of "z means z, draw depth means draw depth", and 2. the use of Extract as the sync point between sim logic and draw logic, which was the entire point of it existing.

nfagerlund commented 1 year ago

Ah wait, first I have to make it so I can display a shadow... and it needs an origin point... which means I need to be able to load ase's with zero tags for completely neutral etc. etc. Hmm. Extend the VariantName / compassdir enum?

nfagerlund commented 1 year ago

Nah, I just used the existing Dir::Neutral for the case of a one-variant ase file. Used a closure to organize that loader code for handling both cases... and made a shortcut way to add a shadow to a lil guy via just a marker struct. So I think we're on track for reorganizing translation and Depth! Bet I'll learn some shit there.

nfagerlund commented 1 year ago

I got this working all right! (In the dec22-fake-z-fightin branch) Ended up having to iterate over an existing vec of ExtractedSprites, but like I put in a TODO comment -- the extract_sprites system and the SpritePlugin's Plugin::build() impl are both quite short, so I could just replace them with a custom sprite plugin of my own, re-use all the rest of the machinery in there, and do the actual relevant work in a single extract pass.