📜 ⬆️ ⬇️

XNA Draw: write deferred lighting to three sources using a shader

Hi habravchanam!

Long time I did not write on habr: study, the session is coming, you know. Today I will try to tell you how to implement Deferred Lighting (deferred lighting) using normal mapping for three light sources in XNA, while using the Reach-profile and Shader model 2.0 .
Let me remind you that earlier we already touched on the topic of shaders: here . The rest is under the cut, video and demo there.


')




In this part:

Theory


Why exactly three light sources at the same time? Shaders have limitations: in Shader model 2.0 , there can be no more than 64 arithmetic operations in one pass of the shader. To use light sources more than three in one pass - Shader model 3.0 is required, it supports 512 arithmetic operations, approximately ~ 30 sources per pass. But for model 3.0 you need a hidef profile, and for it you need a DirectX10-suitable video card. Therefore, we confine ourselves to three sources (and moreover, what prevents to draw sources in several passes? Therefore, three sources are the restriction of the entire shader, in the article we will consider only one pass).

By the way, the comparison of shader models:



More details can be found here .

The limitations of pixel shaders of the second model seem to be sorted out.

Now let's look at another strange word: " deferred lighting ".

Deferred lighting

The main difference between deferred lighting and shading from standard lighting methods is that these methods immediately write the result of the shader to the framebuffer color.

If it is very easy to explain, then we count all the pixels from the light source and immediately write it to the framebuffer, in our case there will be four sums to the framebuffer (ambient + the result of three istons)

Very briefly, what is deferred lighting, we figured out, it remains to understand what the normal mapping.

Normal mapping

Also known in the common people is a map of texture normals, for those who still think: the normal is perpendicular to the surface.

Let's see what can be won with it? I will give an example of lighting using the normal map and without. And then I will explain how this works.

This is how our scene looks without lighting:


So our scene looks like using lighting (without using Normal Mapping):


So our scene looks like using lighting (using Normal Mapping):


As you can see, the result on the face, the scene with Normal Mapping is like a “volumetric” one and it looks more realistic, especially in dynamics (when moving lights).
Let's take a look at the textures themselves.

The texture itself ( Color map ):



Normal Map Texture:



On the second texture, you won’t understand what’s what, remember the displacement-shader article ? There we transmitted through R, G channels information about how to bend a pixel, and here, we transmit with the help of R, G, B information about the normals, i.e. R = X, G = Y, B = Z. And already in the shader we use the X, Y, Z coordinates when calculating the lighting, simple, yes?

And if the reader again did not understand anything, even after seeing the word normal for the first time and got an explanation for it, then using the normal map, the texture gets a 3D representation for lighting.

And yes, about creating a normal map, it’s almost impossible to draw them, they are either removed from the high-poly model, or generated from the texture itself (for example, a plug-in for photoshop).

With a theory like a bit sorted out, let's try to implement it all the code.

Practice


Here I will give pieces of code with comments.
Create a light source view, class LightEmmiter :
public class LightEmmiter { //      : X, Y, Z public Vector3 position; //   public Vector3 color; //   (  — ) public float corrector; //   public float radius; //       internal void UpdateEffect(EffectParameter effectParameter) { effectParameter.StructureMembers["position"].SetValue(position); effectParameter.StructureMembers["color"].SetValue(color * corrector); effectParameter.StructureMembers["invRadius"].SetValue(1f / radius); } } 


Now we are working with Game1 (main class):
Create variables:
 Texture2D texture; // Color  ( ) Texture2D textureNormal; // Normal  private Effect deferred; //  SpriteFont spriteFont; // ,   Debug- EffectParameter lightParameter; //     private float lightRadius; //     private float lightZ; //   Z-  private float lightC; //     LightEmmiter[] lights = new LightEmmiter[2]; //   


Now we initialize all this in the LoadContent method:
 texture = Content.Load<Texture2D>("test1"); //   textureNormal = Content.Load<Texture2D>("test1_map"); //   deferred = Content.Load<Effect>("deferred"); //   spriteFont = Content.Load<SpriteFont>("default"); //   lightRadius = 320f; //    lightZ = 50f; //   Z- lightC = 1f; //    lights[0] = new LightEmmiter(); lights[0].position = new Vector3(20, 30, 0); lights[0].radius = lightRadius; lights[0].corrector = lightC; lights[0].color = new Vector3(1f, 0f, 0f); lights[1] = new LightEmmiter(); lights[1].position = new Vector3(20, 30, 0); lights[1].radius = lightRadius; lights[1].corrector = lightC; lights[1].color = new Vector3(1f, 1f, 1f); 


We update Draw :
 //     RenderTarget —  GraphicsDevice.Clear(Color.LightSkyBlue); GraphicsDevice.SetRenderTarget(null); //     lights[0].position = new Vector3(Mouse.GetState().X, Mouse.GetState().Y, lightZ); lights[0].radius = lightRadius; lights[0].corrector = lightC; lights[1].position = new Vector3(800 - Mouse.GetState().X, 480 - Mouse.GetState().Y, lightZ); lights[1].radius = lightRadius; lights[1].corrector = lightC; //   deferred.CurrentTechnique = deferred.Techniques["Deferred"]; //    deferred.Parameters["screenWidth"].SetValue(GraphicsDevice.Viewport.Width); deferred.Parameters["screenHeight"].SetValue(GraphicsDevice.Viewport.Height); deferred.Parameters["ambientColor"].SetValue(new Vector3(1, 1, 1) * 0.1f); deferred.Parameters["numberOfLights"].SetValue(2); deferred.Parameters["normaltexture"].SetValue(textureNormal); //    lights-   lightParameter = deferred.Parameters["lights"]; //  lights-    for (int i = 0; i < lights.Length; i++) { LightEmmiter l = lights[i]; l.UpdateEffect(lightParameter.Elements[i]); } //   (   ) foreach (EffectPass pass in deferred.CurrentTechnique.Passes) { spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState.CullCounterClockwise); pass.Apply(); spriteBatch.Draw(texture, new Rectangle(0, 0, 800, 480), Color.White); spriteBatch.End(); } //  Debug- spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState.CullCounterClockwise, null); //800 480 spriteBatch.DrawString(spriteFont, "lightRadius: " + lightRadius + "\nlightCorrector: " + lightC + "\nligthZ: " + lightZ, new Vector2(10, 10), Color.LightYellow); spriteBatch.End(); 


All that remains is the most important thing; we create the shader deferred.fx , listing:
 //   ,   struct Light { float3 position; float3 color; float invRadius; }; //   texture normaltexture; // -      3-  int numberOfLights; Light lights[3]; //   float3 ambientColor; // ,   float screenWidth; float screenHeight; //  Color- () sampler ColorMap : register(s0); //  Normal- sampler NormalMap : samplerState { Texture = normaltexture; MinFilter = Linear; MagFilter = Linear; AddressU = Clamp; AddressV = Clamp; }; //      float3 CalculateLight(Light light, float3 normal, float3 pixelPosition) { //  float3 direction = light.position - pixelPosition; float atten = length(direction); direction /= atten; //      float amount = max(dot(normal, direction), 0); atten *= light.invRadius; //  ,  modifer       ,         float modifer = max((1 - atten), 0); //     return light.color * modifer * amount; } float4 DeferredNormalPS(float2 texCoords : TEXCOORD0) : COLOR { float4 base = tex2D(ColorMap, texCoords); //    color-   texCoords float3 normal = normalize(tex2D(NormalMap, texCoords) * 2.0f - 1.0f); //   X,Y,Z  -   texCoords     : -1, 1. //    float3 pixelPosition = float3(screenWidth * texCoords.x, screenHeight * texCoords.y,0); //  - float3 finalColor = 0; for (int i=0;i<numberOfLights;i++) { //          finalColor += CalculateLight(lights[i], normal, pixelPosition); } //   ,  *     ,  multiply          return float4((ambientColor + finalColor) * base.rgb, base.a); } technique Deferred { pass Pass0 { PixelShader = compile ps_2_0 DeferredNormalPS(); } } 


That's all, in this article - I will not tell you how to realize an infinite number of light sources through multiple passes of shaders, you can do it yourself;)

Video demonstration of lighting:


Link to demo (exe): here .
Link to source (project, VS2010): here .

Also, I would like to express special thanks to the lazychaser for helping with the analysis of the material and leading to a pure virgin path.

Good luck;)

Source: https://habr.com/ru/post/134819/


All Articles