light masks implementation like CryEngine

Started by
25 comments, last by jcabeleira 14 years, 5 months ago
Hello, I've been reading some articles about the shading and shadowing implementations on the CryEngine, and from what I read, they separate both of them to reduce the number of shader combinations. This way they avoid writting a huge amount of shaders that do the same thing except for the shadow technique (e.g. phong shading with no shadows, phong with point light shadowmaps, phong with cascaded shadowm maps. you get the point...). I'd like to implement the same thing but I'm concerned about the complications it brings to my engine's rendering pipeline. If I use the old technique of performing both the shading and shadowing on the same shader, then the rendering looks like this: - render light shadowmap - render light pass with shading and shadowing while aditively blending it directly into the frame buffer. Which couldn't be any simpler! But if separate the shading and shadowing, I supose the rendering would have to be something like this: - render light shadowmap - render light pass with shading to auxiliary frame buffer (can't additively blend to main frame buffer because the light doesn't contain shadows yet) - render the shadows to auxiliary frame buffer using modulation blending to mask the light (we either can use forward rendering which increases the draw call, or use deferred techniques like cryengine) - now that the auxiliary frame buffer contains light masked by shadows, addititively blend it into the main frame buffer Is this the simplest way or am I missing something? Thanks.
Advertisement
I recently implemented the same technique in my engine, so my rendering pipeline went from:

- render geometry for ambient term and simple lighting
- for each shadowmapped light N
- - render geometry for light N's shadowmap
- - render geometry for light N with shadowmap computation (using additive blending)

to:

- render geometry for ambient term and simple lighting
- for each shadowmapped light N (Max: 4)
- - render geometry for light N's shadowmap
- for N: 0 to 3
- - if N = 0 then EnableColorWrite(Red)
- - if N = 1 then EnableColorWrite(Green)
- - if N = 2 then EnableColorWrite(Blue)
- - if N = 3 then EnableColorWrite(Alpha)
- - render shadowmap computation to shadowmap buffer
- render the four shadowmapped lights at once, modulating with shadowmap buffer

So I use custom color writes to get a shadowmap buffer with the result for each light on a single color channel. Then I choose the channel to do the modulation based on the current light index (0: red, 1: blue, etc.).

I think my rendering method is very similar to yours, except that I render the shadow map buffer first. This allows to ignore shading on pixels that I alredy know are in shadow (this is specially useful if you're pixel bound). This technique is also useful to render the four lights at once (on a single pass) without exceeding the ps20 instruction limit. The shadowmap computation, as you say, can be done using both forward or deferred rendering.

HTH =)

[Edited by - jpventoso on October 25, 2009 11:55:33 PM]
What they do is actually a bit simpler than you're describing. They simply render the shadow map, then they do a full-screen pass that samples the shadow map does the depth comparison (the pixel position is reconstructed from a depth texture, which is created in an earlier depth-only pass) and output the shadow occlusion value to a screen-sized texture. Then when the geometry is rendered "for real" with lighting, the shadow occlusion texture is sampled and the value is used to attenuate the lighting value calculated in the shader.

I made a sample that does this using XNA, if you're interested.
Hello folks,

I am interested in the same technique. @MJP: do I understand it correctly that they are saving all shadowmaps in one texture that they sample from afterwards? So that you have only one shadowbuffer that you sample from during light accumulation pass?

thanks
You could multiply by the falloff of the light and endup with even less pixels to calculate, don't know how much of a performance gain this would make, if any, but worth a go?

@mokaschitta: you could do it with one shadow buffer, however if your implementing any kind of shadow caching you'll need one per light, obviously :P. Depends on your architecture I guess.
pushpork
Im also interested in this too, am i understanding it correctly?


1. Render whole scene as depth only write to depth buffer
2. Create shadowmaps for each light, (could be a 1 four channel rendertarget for 4 lights, or 2 four channel rendertargets for 8 lights etc etc)
3. Create a third render target, and to this render the shadows in image space. ie go throught all the lights in turn and compare against the depth map and write result to our third render target.
4. Blend the this third rendertarget into our lighting shaders to produce the shadows, where the NDC space for the pixel is converted to UV [0,1] coordinates, so you can then sample the third rendertarget for the shadowing component

questions.

1. when performing stage 3, arent you going to have to do that with a pass for each light? as you have to perform a different world space to light space multiplication so you're comparing values that are in the same space. Or i guess you could just pass in a matrix array of all the light inverse matrices so you can do it all in one pass.

2. Am i right in also thinking stage 2 could be done in one pass, as again you could just pass in a matrix array of all your lights and do something like this for 8 lights.

rendertarget1.r = (Light1WVP * Vertex).depth;
rendertarget2.g = (Light2WVP * Vertex).depth;
rendertarget3.b = (Light3WVP * Vertex).depth;
rendertarget4.a = (Light4WVP * Vertex).depth;

rendertarget2.r = (Light5Transform * Vertex).depth;
rendertarget2.g = (Light6Transform * Vertex).depth;
rendertarget2.b = (Light7Transform * Vertex).depth;
rendertarget2.a = (Light8Transform * Vertex).depth;

or like this for linear depth


rendertarget1.r = (Light1WV * Vertex).depth;
rendertarget2.g = (Light2WV * Vertex).depth;
rendertarget3.b = (Light3WV * Vertex).depth;
rendertarget4.a = (Light4WV * Vertex).depth;

rendertarget2.r = (Light5WV * Vertex).depth;
rendertarget2.g = (Light6WV * Vertex).depth;
rendertarget2.b = (Light7WV * Vertex).depth;
rendertarget2.a = (Light8WV * Vertex).depth;
Quote:Original post by MJP
What they do is actually a bit simpler than you're describing. They simply render the shadow map, then they do a full-screen pass that samples the shadow map does the depth comparison (the pixel position is reconstructed from a depth texture, which is created in an earlier depth-only pass) and output the shadow occlusion value to a screen-sized texture. Then when the geometry is rendered "for real" with lighting, the shadow occlusion texture is sampled and the value is used to attenuate the lighting value calculated in the shader.


I understood your explanation except for the last step. The last step consists in rendering the geometry with lighting, modulating with the shadow texture and additively blending the result to the main frame buffer, all at once?

Don't you need an intermediate step before you blend the result into the main frame buffer?
I believe you have to shade the geometry to an auxiliary frame buffer, then apply a full screen quad that samples the light mask and modulates against the auxiliary frame buffer, and then you additively blend the auxiliary buffer contents to the main frame buffer, right?

Unless, the lighting shaders sample from the light mask directly, and in that case no intermediate steps are needed and the rendering can be directly bended into the main frame buffer.

Quote:Original post by jcabeleira
Unless, the lighting shaders sample from the light mask directly, and in that case no intermediate steps are needed and the rendering can be directly bended into the main frame buffer.


Yes, this is what I meant. So in your shader you have something like this...

float shadowOcclusion = tex2D(ShadowOcclusionSampler, screenTexCoord);float3 lighting = CalcLighting() * shadowOcclusion;return float4(lighting, 1.0f);

Quote:Original post by MJP
Quote:Original post by jcabeleira
Unless, the lighting shaders sample from the light mask directly, and in that case no intermediate steps are needed and the rendering can be directly bended into the main frame buffer.


Yes, this is what I meant. So in your shader you have something like this...

float shadowOcclusion = tex2D(ShadowOcclusionSampler, screenTexCoord);float3 lighting = CalcLighting() * shadowOcclusion;return float4(lighting, 1.0f);


Alright, that's the detail I was missing. Thank you very much. :D
Quote:Original post by maya18222
1. Render whole scene as depth only write to depth buffer
2. Create shadowmaps for each light, (could be a 1 four channel rendertarget for 4 lights, or 2 four channel rendertargets for 8 lights etc etc)
3. Create a third render target, and to this render the shadows in image space. ie go throught all the lights in turn and compare against the depth map and write result to our third render target.
4. Blend the this third rendertarget into our lighting shaders to produce the shadows, where the NDC space for the pixel is converted to UV [0,1] coordinates, so you can then sample the third rendertarget for the shadowing component


That's exactly what I'm doing right now, and it works fine (so far) =)

Quote:Original post by maya18222
1. when performing stage 3, arent you going to have to do that with a pass for each light? as you have to perform a different world space to light space multiplication so you're comparing values that are in the same space. Or i guess you could just pass in a matrix array of all the light inverse matrices so you can do it all in one pass.


Exactly, I pass an array of matrices to the shader (and then transform the world space vertices to light space on the pixel shader, inside the for..loop).

Quote:Original post by maya18222
2. Am i right in also thinking stage 2 could be done in one pass, as again you could just pass in a matrix array of all your lights and do something like this for 8 lights.


I think that wouldn't work because, in standard shadowmapping, your vertex shader needs to output a single POSITION register. Maybe it could be done using deferred techniques?

Another thing that I found interesting about this technique is that you can perform a full-screen blur on all shadowmaps in one pass (of course you'll need to take into account depth difference in order to keep edges sharp):

Image Hosted by ImageShack.us

Cheers,
Juan Pablo

This topic is closed to new replies.

Advertisement