godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.16k stars 97 forks source link

Add a nearest-neighbor filtering option for 2D light shading and shadows #8217

Open dancing-head opened 1 year ago

dancing-head commented 1 year ago

Describe the project you are working on

A 2D isometric pixel art game which uses Godots 2D light and shadows features.

Describe the problem or limitation you are having in your project

Currently lighting and shadows use interpolation and blending to avoid a pixelated effect. This works well but means that in pixelart games the shadows will appear to be in a higher resolution to the art of the game.

It is currently possible to use viewport as a stretch mode and upscale a low resolution rendering of shadows / light. This would achieve a similar effect for shadows but using this stretch mode comes with other limitations which may not be desirable. These include the inability to achieve smooth diagonal movement.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

A project wide setting could be added. Once activated, shadows and light could be rendered using nearest neighbour and not use any interpolation or blending.

This would give the desired pixelated effect.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

A project setting under General / Rendering / Lights and Shadow could be used to ensure that light and shadow are rendered using nearest neighbour scaling and do not interpolate / blend.

The setting could be an additional option on the preexisting quality flags for directional or positional shadows or a new flag in this section.

If this enhancement will not be used often, can it be worked around with a few lines of script?

Any 2D pixel art game using an isometric perspective will require diagonal movement and this is extremely unsatisfying when stretch mode is set to viewport.

I dont believe there is any way to achieve this using the core engine features while using a stretch mode which allows for smooth diagonal movement in a 2D game.

Is there a reason why this should be core and not an add-on in the asset library?

The engine is likely already doing what is desired. The addition of this option would be to stop the engine doing something undesirable in certain use cases.

Calinou commented 1 year ago

Right now, you can decrease the 2D shadow resolution in the project settings, but there are two issues:

image

What you want is probably a downsampling shader that performs pixelation at the same time, so you have pixelated shadows but without artifacting.

Another reason you want something that pixelates the shadow buffer is that changing the shadow sampler filter to nearest won't do the trick on its own anyway:

image

https://github.com/godotengine/godot/blob/f71f4b80e32f6e98a4cd3cb1c06071223297e8fc/servers/rendering/renderer_rd/renderer_canvas_render_rd.cpp#L2616-L2617

dancing-head commented 1 year ago

What you want is probably a downsampling shader that performs pixelation at the same time, so you have pixelated shadows but without artifacting.

Hello,

Thanks for the thoughtful reply.

I would have assumed I should only perform downsampling on the shadow itself though. Otherwise wouldnt you just get the same results as using a viewport?

I dont think there is any way of getting the shadow texture itself though. Am I mistaken?

Since I couldnt get the shadow I assumed the best way would be to disable shadows and create a shader that recreates shadows to the specification I desired using the SDF created by light occluders. There doesnt seem to be a way to distinguish SDFs by the light mask used by their occluders however so its not immediately obvious how this can be done either. I was expecting to only get the SDF for colliders using the same light mask as the light source in the current light pass but this doesnt seem to be the case. For that matter I get SDFs for occluders even if the occluders SDF Collision flag is false which I wasnt expecting either.

Am I missing something obvious?

Calinou commented 1 year ago

I dont think there is any way of getting the shadow texture itself though. Am I mistaken?

It's not exposed, so it can't be done using a custom shader (and you wouldn't want to do this using a script as this needs to be performed on the GPU).

For that matter I get SDFs for occluders even if the occluders SDF Collision flag is false which I wasnt expecting either.

Do particle collisions disable themselves in this case? If not, please report this on the main Godot repository as it's surely a bug.

dancing-head commented 1 year ago

Do particle collisions disable themselves in this case? If not, please report this on the main Godot repository as it's surely a bug.

Im not sure what you mean by this. What particle collisions should be disabled? I was just checking the values of texture_sdf() inside light() passes and neither the light mask nor the collision flag of the occluder made any difference as to whether the light source gave negative values once it was inside an occluder. Is that what you mean by collisions? If not what do you mean?

Is it not a bug that the mask is ignored also? I cant see how to use SDFs if there is no way of knowing which one a ray of light should interact with. I feel like I must be missing something.

Im quite inexperienced with Godot also. How would you recommend I solve this issue? Do I need to make a change to the engine itself to expose the shadow texture and apply a shader to it? Is there a better way?

Calinou commented 1 year ago

Im quite inexperienced with Godot also. How would you recommend I solve this issue? Do I need to make a change to the engine itself to expose the shadow texture and apply a shader to it? Is there a better way?

You will need to modify the engine but expect this to be nontrivial, as you need to run a shader on the shadow texture.

dancing-head commented 1 year ago

Thanks for your opinion. Ill give it a go.

Thinking about it though, if Im going to go to the effort of building a custom version I may as well make SDFs from occluders be usable, at least as I see it. That would allow for more sophisticated effects later. I think Ill try and change it so they respect the various masks for each pass.

Im completely new to this code however. If you have an entry point for where the various light passes, especially those from point lights, are in the engine that would be extremely useful.

For that matter if you can think of a reason not to do this that would be useful also.

Calinou commented 1 year ago

Im completely new to this code however. If you have an entry point for where the various light passes, especially those from point lights, are in the engine that would be extremely useful.

Relevant shaders (for Vulkan-based renderers, i.e. Forward+ and Mobile):

On the C++ side, this is handled in:

Use Ctrl + F shadow within that code, and you should find the relevant portions quickly.

dancing-head commented 11 months ago

Thanks a lot for your pointers. They were very helpful.

Its possible that there is a better way to do this but I modified the renderer_canvas_render_rd (and related classes) to send an array of sdfs to the canvas.glsl shader. The first element is the regular sdf and the rest of the array is 1 sdf per light source with the sdf per light sources only including appropriate occluders.

The texture_sdf function is modified to take the first element of the array during the fragment function and the n+1th element of the array for the nth light pass for the light function.

The C++ code is straightforward enough, rusty as I am, but the shader code is a little too hacky for me to be comfortable suggesting a change, especially since I further modified it to change one of the filters to be a "no shadow" filter in order to have a custom shader create a pixelated shadow but still use the shadow mask.

I think it would be far easier for users in general if the light function worked as above although there is surely a better implementation than I found. Apart from my use case, any custom shader that uses sdfs is going to have difficulty when the game relies on masks for the shadows as the engine currently works.

Do you think I should close this and open a new, more precisely worded issue or do you disagree that the engine should work this way for the light function of the CanvasItemShader?

Calinou commented 11 months ago

Do you think I should close this and open a new, more precisely worded issue or do you disagree that the engine should work this way for the light function of the CanvasItemShader?

I suggest opening a separate proposal for this.