Game rendering technology: forward rendering vs delayed rendering vs forward+ rendering (2)

GTA5

2 Forward rendering

Forward rendering is the simplest of the three lighting techniques and the most common technique in game graphics rendering. For this reason, and the most expensive technique for lighting calculations, it does not allow for a large number of dynamic light sources in the scene.

Most graphics engines that use forward rendering will use some techniques to simulate a large number of light sources in the scene. For example, lightmap (light map) and lightProbe (light probe) both use static light sources placed in the scene to pre-calculate the lighting contribution. method and stores these lighting contributions in a texture for loading at runtime. Unfortunately, lightmap and lightprobe cannot simulate dynamic light sources in the scene, because the lightmaps generated by these light sources are often discarded at runtime.

In this experiment, the results of forward rendering are used as a baseline for comparison with two other rendering techniques. Forward rendering technology is also used to build a baseline for performance comparison with other rendering technologies.

Many methods in forward rendering will be reused in deferred and forward+ rendering. For example, the vertex shader in forward rendering will also be used in deferred rendering and forward+ rendering. The same method for calculating final lighting and material shading is used for all rendering techniques.

In the next section, I describe the implementation details of forward rendering.

2.1 Vertex shader

The vertex shader is common to all rendering technologies. In this experiment, only static geometry is supported, no skeletal animations and surfaces, which require different vertex shaders. The vertex shader is as simple as possible to support some functions in the pixel shader, such as normal mapping.

Before showing the vertex shader code, I will describe the data structures used by the vertex shader.

// CommonInclude.hlsl
140 struct AppData
141 {
142 float3 position : POSITION;
143 float3 tangent : TANGENT;
144 float3 binormal : BINORMAL;
145 float3 normal : NORMAL;
146 float2 texCoord : TEXCOORD0;
147 };

The AppData structure defines the data that needs to be sent to the GPU by the application code. In addition to the normal vector for normal mapping, we also need to send the tangent vector. The binormal (or bitangent) vector is optional. Tangents and binormals can be generated by the 3D artist when creating the model, or by the model loader. In this case, I used the Open Asset Import Library [7] to generate tangents and bitangents if they were not generated by the 3D artist.

In the vertex shader, we also need to know how to transform the model space vector into the view space vector required in the pixel shader. In order to achieve this transformation, we need to send the world, view and projection matrices to the vertex shader. In order to store the variables needed for each model in these vertex shaders, I will create a constant buffer.

// CommonInclude.hlsl
149 cbuffer PerObject : register( b0 )
150 {
151 float4x4 ModelViewProjection;
152 float4x4 ModelView;
153 };

Since I don't need to store the world matrix separately, I precompute the combined model and view matrices, and the combined model, view, and projection matrices in the application and send them to a separate constant buffer for the vertex shader.

The output of the vertex shader (i.e. the input to the pixel shader) looks like this:

CommonInclude.hlsl
181 struct VertexShaderOutput
182 {
183 float3 positionVS : TEXCOORD0; // View space position.
184 float2 texCoord : TEXCOORD1; // Texture coordinate
185 float3 tangentVS : TANGENT; // View space tangent.
186 float3 binormalVS : BINORMAL; // View space binormal.
187 float3 normalVS : NORMAL; // View space normal.
188 float4 position : SV_POSITION; // Clip space position.
189 };

The VertexShaderOutput structure is used to pass transformed vertex attributes to the Pixel shader. The vs suffix member indicates that the vector is in view space. I chose to do all lighting in view space rather than world space because it's easier to do deferred rendering and forward+ rendering in view space coordinates.

The vertex shader is very straightforward and short. Its only goal is to transform the model space vectors passed by the application into the view space vectors used in the pxiel shader.

The vertex shader must also calculate the position in clip space required by the rasterizer. The SV_POSITION output for the vertex shader is used for the clip space position, but this semantics can also be used as input to the pixel shader. variable. When SV_POSITION is used as an input to a pixel shader, this value represents the position in screen space [8]. In deferred rendering and forward+ shaders, I will use this semantics to obtain the position of the current pixel in screen space.

// ForwardRendering.hlsl
3 VertexShaderOutput VS_main( AppData IN )
4 {
5 VertexShaderOutput OUT;
6
7 OUT.position = mul( ModelViewProjection, float4( IN.position, 1.0f ) );
8 
9 OUT.positionVS = mul( ModelView, float4( IN.position, 1.0f ) ).xyz;
10 OUT.tangentVS = mul( ( float3x3 )ModelView, IN.tangent );
11 OUT.binormalVS = mul( ( float3x3 )ModelView, IN.binormal );
12 OUT.normalVS = mul( ( float3x3 )ModelView, IN.normal );
13 
14 OUT.texCoord = IN.texCoord;
15 
16 return OUT;
17 }

You'll notice that I multiply the input vector using a matrix (matrix first, vector last), which means the matrix is ​​stored in column-major order. Before DirectX 10, matrices in HLSL were loaded in row-major order, and the input vector was multiplied by the matrix last (vector first, matrix last). After DirectX 10, the matrix was loaded by default. Main column order. You can change the default order by specifying the main row modifier at the matrix declaration [9].

2.2 Pixel Shader

The pixel shader calculates all lighting and shading to determine the final color of a screen pixel. The lighting equation used in the Pixel shader refers to Textures and Lighting in DirectX 11. If you are not familiar with the lighting equation, you need to read this article before continuing.

The pixel shader uses several structures to do this work. The Material structure stores all the information describing the surface material of the shaded object, and the Light struct contains all the parameters describing the scene lighting.

2.2.1 Material

Material defines all properties used to describe the surface of the current shaded object. Because some material properties may have related textures (such as diffuse textures, specular textures, or normal maps), we will also use materials to indicate whether this texture is rendered. on this object.

// CommonInclude.hlsl
10 struct Material
11 {
12 float4 GlobalAmbient;
13 //-------------------------- ( 16 bytes )
14 float4 AmbientColor;
15 //-------------------------- ( 16 bytes )
16 float4 EmissiveColor;
17 //-------------------------- ( 16 bytes )
18 float4 DiffuseColor;
19 //-------------------------- ( 16 bytes )
20 float4 SpecularColor;
21 //-------------------------- ( 16 bytes )
22 // Reflective value.
23 float4 Reflectance;
24 //-------------------------- ( 16 bytes )
25 float Opacity;
26 float SpecularPower;
27 // For transparent materials, IOR > 0.
28 float IndexOfRefraction;
29 bool HasAmbientTexture;
30 //-------------------------- ( 16 bytes )
31 bool HasEmissiveTexture;
32 bool HasDiffuseTexture;
33 bool HasSpecularTexture;
34 bool HasSpecularPowerTexture;
35 //-------------------------- ( 16 bytes )
36 bool HasNormalTexture;
37 bool HasBumpTexture;
38 bool HasOpacityTexture;
39 float BumpIntensity;
40 //-------------------------- ( 16 bytes )
41 float SpecularScale;
42 float AlphaThreshold;
43 float2 Padding;
44 //--------------------------- ( 16 bytes )
45 }; //--------------------------- ( 16 * 10 = 160 bytes )

GlobalAmbient is used to describe the ambient light properties that act globally on all objects. Technically, this variable should be a global variable (not assigned to a single object), but because there is only one material in a pixel shader at a time, I Thought this would be a better place to store it.

Ambient, emissive, diffuse and specular colors have the same meaning as in textures and lighting in DirectX 11, so they will not be explained further here.

Reflectance is used to represent the amount of reflection color that should be mixed with the diffuse color. This requires an environment map (cube texture) to implement, which will not be used in this experiment.

Opacity is used to determine the total opacity of an object. This value can be used to make the object appear transparent. This property is used to render semi-transparent objects in the transparent pass. If the value is less than 1 (1 means completely opaque, 0 means completely transparent ), the object will be considered transparent and the object will be rendered in the transparency pass, not in the opaque pass.

The variable SpecularPower is used to determine how shiny an object looks. This variable is explained in detail in Textures and Lighting in DirectX 11.

The variable HasTexture defined in lines 29-38 indicates whether the object uses the relevant texture for rendering. If this parameter is true, the corresponding texture will be sampled, and the sampled texel (texel, distinguished from pixel) will be compared with the corresponding Material colors are mixed.

BumpIntensity is used to scale the height values ​​obtained from the bump map (not to be confused with normal mapping, normals are not scaled) to soften or enhance the undulations of the object's surface. Most often, models will use normal maps to add detail to surfaces without tessellation, but a heightmap can also be used to do the same thing. If the model uses a bump map, the material's HasBumpTexture property will be set to true, in which case the model uses bump mapping instead of normal mapping.

SpecularScale is used to scale the specular power value read from the specular intensity texture. Because textures usually hold unsigned normalized values, values ​​sampled from textures are read as floating point numbers in the range [0..1]. A specular intensity of 1.0 is meaningless, so the specular intensity read from the texture is scaled by SpecularScale before being used in the final lighting calculation.

AlphaThreshold is used to discard pixels with opacity below a certain value, usually using "discard" in the pixel shader. This can be used for "cut-out" materials where objects do not require alpha for blending, but do have holes in the object (such as a linked fence).

Padding is used to explicitly add 8 bytes to fill the material structure. Although HLSL will implicitly add this padding (8 bytes) to the structure to ensure that the structure is a multiple of 16 bytes, explicitly adding padding will make it more explicit that the size and alignment of the structure will be consistent with the corresponding C++ copy. consistent.

Material properties are passed to the pixel shader through a constant buffer.

// CommonInclude.hlsl
155 cbuffer Material : register( b2 )
156 {
157 Material Mat;
158 };

Constant buffer and buffer register slot allocation are used for all pixel shaders in this article.

2.2.2 Texture

The material already supports 8 different types of textures

  1. Environment map-Ambient
  2. Self-illuminating map-Emissive
  3. Diffuse map-Diffuse
  4. Specular map-Specular
  5. Specular intensity map-SpecularPower
  6. Normal map-Normals
  7. Bump map-Bump
  8. Opacity map-Opacity

Not all scene objects will use all texture slots (normal maps and bump maps are mutually exclusive, so they may reuse the same texture slot), it depends on the 3D artist to let the scene Which textures are used for the model. The application loads a material-related texture, a texture parameter and an associated texture slot declared for each of these material properties.

// CommonInclude.hlsl
167 Texture2D AmbientTexture : register( t0 );
168 Texture2D EmissiveTexture : register( t1 );
169 Texture2D DiffuseTexture : register( t2 );
170 Texture2D SpecularTexture : register( t3 );
171 Texture2D SpecularPowerTexture : register( t4 );
172 Texture2D NormalTexture : register( t5 );
173 Texture2D BumpTexture : register( t6 );
174 Texture2D OpacityTexture : register( t7 );

In each pixel shader of this article, texture slots 0-7 are reserved for these textures.

2.2.3 Lighting

The Light structure stores all the information needed to define a light in the scene. Spot lights, point lights and directional lights are not separated into different structures, all the necessary properties to define any type of light are stored in a single structure.

CommonInclude.hlsl
47 struct Light
48 {
49 /**
50 * Position for point and spot lights (World space). 
51 */ 
52 float4 PositionWS;
53 //--------------------------------------------------------------( 16 bytes ) 
54 /** 
55 * Direction for spot and directional lights (World space). 
56 */ 
57 float4 DirectionWS; 
58 //--------------------------------------------------------------( 16 bytes ) 
59 /** 
60 * Position for point and spot lights (View space). 
61 */ 
62 float4 PositionVS; 
63 //--------------------------------------------------------------( 16 bytes ) 
64 /** 
65 * Direction for spot and directional lights (View space). 
66 */ 
67 float4 DirectionVS; 
68 //--------------------------------------------------------------( 16 bytes ) 
69 /** 
70 * Color of the light. Diffuse and specular colors are not seperated. 
71 */ 
72 float4 Color; 
73 //--------------------------------------------------------------( 16 bytes ) 
74 /** 
75 * The half angle of the spotlight cone. 
76 */ 
77 float SpotlightAngle; 
78 /** 
79 * The range of the light. 
80 */ 
81 float Range; 
82 
83 /** 
84 * The intensity of the light. 
85 */ 
86 float Intensity; 
87 
88 /** 
89 * Disable or enable the light. 
90 */ 
91 bool Enabled; 
92 //--------------------------------------------------------------( 16 bytes ) 
93 
94 /** 
95 * Is the light selected in the editor? 
96 */
97 bool Selected; 
98 
99 /** 
100 * The type of the light.
101 */
102 uint Type;
103 float2 Padding;
104 //--------------------------------------------------------------( 16 bytes )
105 //--------------------------------------------------------------( 16 * 7 = 112 bytes )
106 };

Position and Direction store the position and direction in both world space (_WS_ suffix) and view space (_VS_ suffix). Of course, the position attribute only applies to point lights and spotlights, and the direction attribute only applies to spotlights and directional lights. The reason why only two different spaces are stored at the same time is because the world space is easier to use in the application phase, and then the world space is converted into the view space before being passed to the GPU. This way no extra GPU storage space is needed. to manage multiple light lists. Since 10,000 lights only require 1.12MB of GPU memory, this is a reasonable sacrifice. But minimizing light structures has a positive side to the GPU cache and can improve rendering performance.

In some lighting models, the diffuse and specular lighting contributions are separated. Because the difference is small, we choose not to separate the contributions of the two here, but store the two in the Color variable.

SpotlightAngle is the half-angle of the spotlight cone expressed in angle. Using angle is more intuitive than radian. Of course, the angle of the spotlight will be converted into radians when calculating cosine in the shader.

  • Figure spotlight angle

Range determines the distance the light reaches the surface, and also determines the contribution of the light to the surface. While not entirely physically correct (real lights have a falloff that won't actually be 0), the light needs to have a limited range to implement deferred shading and Forward+ rendering techniques. The units for this range are scene-specific, but a unit of 1 meter will be used here. For point lights, the range is the radius of the sphere that represents the light, and for spotlights, the range is the length of the cone that represents the light. Directional lights don't use ranges because they are considered infinite and point in the same direction.

Intensity is used to adjust the calculated light contribution. By default, this value is 1, which can be used to adjust the brightness of the light.

The Enabled flag can control the turning on or off of lights in the scene. Lights with Enabled set to false will be skipped in the shader.

In this demo, lights can be edited. You can select a light by clicking on it in the demo. Its properties can also be modified. To indicate that a light is selected, the Selected tag will be set to true. When a light is selected in the scene, it will appear darker to indicate that it is selected.

Type is used to specify the type of the light, which can be one of the following:

// CommonInclude.hlsl
6 #define POINT_LIGHT 0
7 #define SPOT_LIGHT 1
8 #define DIRECTIONAL_LIGHT 2

Once again, explicitly add 8 bytes of padding to the Light structure to match the struct layout in C++, and use the structure to meet the 16-byte alignment required by HLSL.

The light array is accessed through StructuredBuffer. Most lighting Shader implementations use constant buffers for storage, but the constant buffer is limited to 64KB in size, which also means that the maximum number of times it can be used before running out of constant memory on the GPU is 570 dynamic light sources. The structured buffer is stored in texture memory, which is limited by the amount of available texture memory provided by the GPU (usually measured in GB on desktop GPUs). Texture memory is fast on most GPUs, so using texture memory to store lights has no performance impact. In fact, on some specific GPUs (NVIDIA GeForce GTX 680), the data is placed in a structured buffer. On the contrary, there is a certain performance improvement in the area.

// CommonInclude.hlsl
176 StructuredBuffer<Light> Lights : register( t8 );

2.3 Pixel Shader Continued

Compared with the vertex shader, the forward rendering pixel shader is relatively more complex. The pixel shader will be explained in detail here because it is the basis of all rendering algorithms in this article.

2.3.1 Material

First, we need to receive all the material properties of the material. If a material contains textures and corresponding components, these textures will be sampled before lighting calculations. After all material properties are initialized, all lights in the scene will be traversed, and the lighting contribution will produce the final pixel color as the material properties are accumulated and adjusted.

ForwardRendering.hlsl
19 [earlydepthstencil]
20 float4 PS_main( VertexShaderOutput IN ) : SV_TARGET
21 {
22 // Everything is in view space.
23 float4 eyePos = { 0, 0, 0, 1 };
24 Material mat = Mat;

The [earlydepthstencil] attribute before the function indicates that the GPU should do early depth and stencil culling [10] first, which will cause the depth/stencil test to be executed before the pixel shader. This attribute cannot be used with shaders that use SV_Depth semantics to modify depth. Because this pixel shader only uses SV_TARGET semantics to output color, early depth/stencil testing can be used to improve performance when a pixel is rejected. Most GPUs will perform an early depth/stencil test even without the [earlydepthstencil] attribute. Although adding this attribute will not have a significant performance impact, I still leave this attribute.

Because all lighting calculations are in view space, the eye position (camera position) is always (0, 0, 0). This is a positive side of using view space, so the camera position does not need to be passed to the shader as another parameter. .

Line 24 copies a material. This is because if there is an associated texture to the material properties, the material properties will be changed in the shader (the corresponding properties will be loaded from the texture). Because material properties are stored in a constant buffer, there is no way to directly update a uniform variable in a constant buffer, so a temporary variable is used.

2.3.1.1 Diffuse

Diffuse color is the first material property read.

// ForwardRendering.hlsl
26 float4 diffuse = mat.DiffuseColor;
27 if ( mat.HasDiffuseTexture )
28 {
29 float4 diffuseTex = DiffuseTexture.Sample( LinearRepeatSampler, IN.texCoord );
30 if ( any( diffuse.rgb ) )
31 {
32 diffuse *= diffuseTex;
33 }
34 else
35 {
36 diffuse = diffuseTex;
37 }
38 }

The default diffuse color is the DiffuseColor in the material. If the material has an associated diffuse texture, the color will be mixed with the color loaded in the diffuse texture. If the color in the material is black (0, 0, 0), the color loaded by the diffuse texture will be used directly. HLSL's built-in _any_ function can be used to determine whether a color channel is not 0.

2.3.1.2 Opacity

Determines the alpha value of the pixel.

ForwardRendering.hlsl
41 float alpha = diffuse.a;
42 if ( mat.HasOpacityTexture )
43 {
44 // If the material has an opacity texture, use that to override the diffuse alpha.
45 alpha = OpacityTexture.Sample( LinearRepeatSampler, IN.texCoord ).r;
46 }

By default, the transparency value (that is, the alpha value) of the fragment is determined by the alpha of the diffuse color. If the material has an associated opacity texture, the red channel (r channel) of the opacity texture will replace the alpha value in the diffuse texture as the alpha value of the diffuse color. In most cases, the opacity texture only stores one channel in the first component of the color, and will also return to the first component when sampled. In order to read values ​​from a single-channel texture, we must read from the red (r) channel, not the alpha channel, since the alpha value in a single-channel texture is always 1.

2.3.1.3 Ambient light and self-illumination (Ambient and Emissive)

The reading of ambient light (Ambient) and self-luminous (Emissive) colors is similar to the diffuse color. The ambient light color also needs to be mixed with the GlobalAmbient variable in the material.

// ForwardRendering.hlsl
48 float4 ambient = mat.AmbientColor;
49 if ( mat.HasAmbientTexture )
50 {
51 float4 ambientTex = AmbientTexture.Sample( LinearRepeatSampler, IN.texCoord );
52 if ( any( ambient.rgb ) )
53 {
54 ambient *= ambientTex;
55 }
56 else
57 {
58 ambient = ambientTex;
59 }
60 }
61 // Combine the global ambient term.
62 ambient *= mat.GlobalAmbient;
63
64 float4 emissive = mat.EmissiveColor;
65 if ( mat.HasEmissiveTexture )
66 {
67 float4 emissiveTex = EmissiveTexture.Sample( LinearRepeatSampler, IN.texCoord );
68 if ( any( emissive.rgb ) )
69 {
70 emissive *= emissiveTex;
71 }
72 else
73 {
74 emissive = emissiveTex;
75 }
76 }

2.3.1.4 Specular Power

Next the highlight intensity is calculated.

// ForwardRendering.hlsl
78 if ( mat.HasSpecularPowerTexture )
79 {
80 mat.SpecularPower = SpecularPowerTexture.Sample( LinearRepeatSampler, IN.texCoord ).r \
81 * mat.SpecularScale;
82 }

If the material has an associated SpecularPower texture, the red component of that texture is sampled and then scaled using SpecularScale in the scale material. In this case, the SpecularPower in the material is replaced by the scaled value in the texture.

2.3.1.5 Normal (Normal)

If the material has either an associated normal map or a bump map, normal mapping or bump mapping will be performed to compute the normal vector. If neither a normal map nor a bump map texture is associated with the material, the input normal is used as-is.

If there is a normal map or bump map associated with the texture, a normal map or a bump map is performed to calculate the normal vector. If there is neither, the input normal is used (from the vertex output in shader).

// ForwardRendering.hlsl
85 // Normal mapping
86 if ( mat.HasNormalTexture )
87 {
88 // For scenes with normal mapping, I don't have to invert the binormal.
89 float3x3 TBN = float3x3( normalize( IN.tangentVS ),
90 normalize( IN.binormalVS ),
91 normalize( IN.normalVS ) );
92
93 N = DoNormalMapping( TBN, NormalTexture, LinearRepeatSampler, IN.texCoord );
94 }
95 // Bump mapping
96 else if ( mat.HasBumpTexture )
97 {
98 // For most scenes using bump mapping, I have to invert the binormal.
99 float3x3 TBN = float3x3( normalize( IN.tangentVS ),
100 normalize( -IN.binormalVS ), 
101 normalize( IN.normalVS ) );
102 
103 N = DoBumpMapping( TBN, BumpTexture, LinearRepeatSampler, IN.texCoord, mat.BumpIntensity );
104 }
105 // Just use the normal from the model.
106 else
107 {
108 N = normalize( float4( IN.normalVS, 0 ) );
109 }

2.3.1.6 Normal Mapping

The function DoNormalMapping uses the TBN (tangent), bitangent/binormal (bitangent/binormal), normal (normal) matrix and normal map to calculate the normal mapping (Normal Mapping).

  • An example of a normal map for a lion's head. [11]
CommonInclude.hlsl
323 float3 ExpandNormal( float3 n )
324 {
325 return n * 2.0f - 1.0f;
326 }
327
328 float4 DoNormalMapping( float3x3 TBN, Texture2D tex, sampler s, float2 uv )
329 {
330 float3 normal = tex.Sample( s, uv ).xyz;
331 normal = ExpandNormal( normal );
332 
333 // Transform normal from tangent space to view space.
334 normal = mul( normal, TBN );
335 return normalize( float4( normal, 0 ) );
336 }

Normal mapping is very simple and is explained in detail in this article Normal Mapping. To put it simply, we only need to sample the normals from the normal map, expand the normals to the [-1..1] range, and then transform them from tangent space to view space by post-multiplying the TBN matrix.

2.3.1.7 Bump Mapping

The principle of bump mapping is similar, except that the normals in the bump texture are not directly stored, but the height values ​​in the [0..1] range. The normal can be generated by calculating the gradient of the height of the bump texture in the U and V coordinate directions. The normal of the texture space is obtained by the cross product of the gradients in the two directions, and then multiplied by the TBN matrix. Transform it from tangent space to view space. A larger (smaller) bump can be produced by scaling the height value read from the bump map.

 Bump texture (left) and corresponding head model (right) [12]

CommonInclude.hlsl
333 float4 DoBumpMapping( float3x3 TBN, Texture2D tex, sampler s, float2 uv, float bumpScale )
334 {
335 // Sample the heightmap at the current texture coordinate.
336 float height = tex.Sample( s, uv ).r * bumpScale;
337 // Sample the heightmap in the U texture coordinate direction.
338 float heightU = tex.Sample( s, uv, int2( 1, 0 ) ).r * bumpScale;
339 // Sample the heightmap in the V texture coordinate direction.
340 float heightV = tex.Sample( s, uv, int2( 0, 1 ) ).r * bumpScale;
341
342 float3 p = { 0, 0, height };
343 float3 pU = { 1, 0, heightU };
344 float3 pV = { 0, 1, heightV };
345 
346 // normal = tangent x bitangent
347 float3 normal = cross( normalize(pU - p), normalize(pV - p) );
348 
349 // Transform normal from tangent space to view space.
350 normal = mul( normal, TBN );
351 
352 return float4( normal, 0 );
353 }

There is no guarantee that the bump mapping algorithm is 100% correct. No relevant resources have been found on how to correctly perform bump mapping. If there is a better way to perform bump mapping, please leave a message for discussion.

If the material does not have an associated normal map or bump map, the normal vector output in the vertex shader is used directly.

Now we have all the data we need to calculate lighting.

2.3.2 Lighting

The lighting calculations for the forward rendering technique are performed in the DoLighting function. This function accepts the following arguments:

The lighting calculation of forward rendering technology is performed in the function DoLighting, which accepts the following parameters:

  • lights: array of light sources (structured buffer).
  • mat: The material properties we calculated earlier.
  • eyePos: Camera coordinates in view space (always (0, 0, 0)).
  • P: The position of the shaded point in view space.
  • N: The normal of the shaded point in view space.

The function DoLighting returns a DoLighting structure containing the diffuse and specular lighting contributions of all lights in the scene.

// ForwardRendering.hlsl
425 // This lighting result is returned by the
426 // lighting functions for each light type.
427 struct LightingResult
428 {
429 float4 Diffuse;
430 float4 Specular;
431 };
432
433 LightingResult DoLighting( StructuredBuffer<Light> lights, Material mat, float4 eyePos, float4 P, float4 N )
434 {
435 float4 V = normalize( eyePos - P );
436 
437 LightingResult totalResult = (LightingResult)0;
438 
439 for ( int i = 0; i < NUM_LIGHTS; ++i )
440 {
441 LightingResult result = (LightingResult)0;
442 
443 // Skip lights that are not enabled.
444 if ( !lights[i].Enabled ) continue;
445 // Skip point and spot lights that are out of range of the point being shaded.
446 if ( lights[i].Type != DIRECTIONAL_LIGHT &&
447 length( lights[i].PositionVS - P ) > lights[i].Range ) continue;
448 
449 switch ( lights[i].Type )
450 {
451 case DIRECTIONAL_LIGHT:
452 {
453 result = DoDirectionalLight( lights[i], mat, V, P, N );
454 }
455 break;
456 case POINT_LIGHT:
457 {
458 result = DoPointLight( lights[i], mat, V, P, N );
459 }
460 break;
461 case SPOT_LIGHT:
462 {
463 result = DoSpotLight( lights[i], mat, V, P, N );
464 }
465 break;
466 }
467 totalResult.Diffuse += result.Diffuse;
468 totalResult.Specular += result.Specular;
469 }
470 
471 return totalResult;
472 }

The gaze vector (V) is calculated from the eye position and the position of the shaded pixel in view space.

The iteration of the light buffer is on line 439. Because disabled light sources and out-of-range light sources will not contribute any lighting, these light sources can be skipped, otherwise the corresponding lighting function will be called according to the light source type.

Each different type of light source will calculate their diffuse and specular lighting contributions. Because the diffuse and specular lighting contributions are calculated in the same way for different types of light sources, I will define functions that do not depend on the light source type to calculate the diffuse and specular lighting contributions.

2.3.2.1 Diffuse Lighting

The function DoDiffuse is very simple and only requires knowledge of the light vector (L) and the surface normal (N).

                                             Figure diffuse lighting

// CommonInclude.hlsl
355 float4 DoDiffuse( Light light, float4 L, float4 N )
356 {
357 float NdotL = max( dot( N, L ), 0 ); 
358 return light.Color * NdotL; 
359 } 

The calculation of diffuse illumination uses the dot product of the light vector (L) and the surface normal (N). The two vectors need to be normalized. The result of the dot product is compared with the color of the light. Multiply to get the lighting contribution of that light.

Next, we calculate the specular contribution of the light.

2.3.2.2 Specular Lighting

The function DoSpecular is used to calculate the specular contribution of the light. In addition to the light vector (L) and the surface normal (N), the function also requires the sight vector (V) to calculate the specular contribution of the light.

                                           Specular Lighting

CommonInclude.hlsl
361 float4 DoSpecular( Light light, Material material, float4 V, float4 L, float4 N )
362 {
363 float4 R = normalize( reflect( -L, N ) );
364 float RdotV = max( dot( R, V ), 0 );
365
366 return light.Color * pow( RdotV, material.SpecularPower );
367 }

Because the light vector L is the vector from the shaded point to the light source, L needs to be negative before calculating the reflection vector (R) so that the vector points from the light source to the shaded point. The dot product of the reflection vector (R) and the view vector (V) is used to calculate the power of the highlight intensity value, which is then modulated using the light color. Remember that the range of the highlight intensity is (0..1) which is meaningless.

2.3.2.3 Attenuation

Attenuation is the decrease in the intensity of light as it moves farther away from the point being colored. In the traditional lighting model, falloff is calculated as the sum of three falloff factors multiplied by the reciprocal of the distance to the light source (as explained in Falloff):

  1. constant decay
  2. Linear attenuation
  3. quadratic decay

However, the attenuation calculated by this method assumes that light will never decay to 0 (light has infinite range). For deferred rendering and forward+, we must be able to represent that the light in the scene has a limited range, so we use a differential method to calculate the attenuation of the light.

A feasible method is to do a linear interpolation from 0 to 1 to calculate the attenuation of the light, where 1 means close to the light source and 0 means the distance from the point to the light source exceeds the range of the light. However, linear attenuation does not look very realistic. In fact, Decay is more like the inverse of a quadratic function.

I plan to use HLSL's built-in smoothstep function, which returns a smooth interpolation between the minimum and maximum values.

                                                   HLSL built-in smoothstep function

// CommonInclude.hlsl
396 // Compute the attenuation based on the range of the light.
397 float DoAttenuation( Light light, float d )
398 {
399 return 1.0f - smoothstep( light.Range * 0.75f, light.Range, d );
400 }

The function smoothstep returns 0 if the distance to the light source (d)/ is less than ¾ of the light range, and 1 if the distance is greater than the light range. By subtracting this value from 1 we get the attenuation we need.

Alternatively, we can adjust the smoothness of the light's falloff by parameterizing 0.75f ​​in the equation above. A smoothing factor of 0.0 should cause the light's intensity to remain 1.0 up to the light's maximum range, while a smoothing factor of 1.0 should cause the light's intensity to be interpolated through the entire range of the light.

Variable falloff smoothing

Now, let's combine diffuse, specular and attenuation factors to calculate the light contribution for different light types.

2.3.2.4 Point Light

Point lights combine attenuation, diffuse and specular to determine the final lighting contribution.

// ForwardRendering.hlsl
390 LightingResult DoPointLight( Light light, Material mat, float4 V, float4 P, float4 N )
391 {
392 LightingResult result;
393
394 float4 L = light.PositionVS - P;
395 float distance = length( L );
396 L = L / distance;
397 
398 float attenuation = DoAttenuation( light, distance );
399 
400 result.Diffuse = DoDiffuse( light, L, N ) * 
401 attenuation * light.Intensity;
402 result.Specular = DoSpecular( light, mat, V, L, N ) * 
403 attenuation * light.Intensity;
404 
405 return result;
406 }

In lines 400 and 401, the diffuse and specular contributions are scaled by attenuation and intensity.

2.3.2.5 Spot Light

In addition to the attenuation factor, the spotlight also has a cone angle. In this case, the intensity of the light is determined by the dot product between the light vector (L) and the direction of the spotlight. If the angle between the light vector and the spotlight direction is smaller than the spotlight cone angle, the point should be lit by the spotlight. Otherwise the spotlight should not provide any lighting to the point being shaded. The DoSpotCone function will calculate the light intensity based on the angle of the light cone.

// CommonInclude.hlsl
375 float DoSpotCone( Light light, float4 L )
376 {
377 // If the cosine angle of the light's direction
378 // vector and the vector from the light source to the point being
379 // shaded is less than minCos, then the spotlight contribution will be 0.
380 float minCos = cos( radians( light.SpotlightAngle ) );
381 // If the cosine angle of the light's direction vector
382 // and the vector from the light source to the point being shaded
383 // is greater than maxCos, then the spotlight contribution will be 1.
384 float maxCos = lerp( minCos, 1, 0.5f );
385 float cosAngle = dot( light.DirectionVS, -L );
386 // Blend between the minimum and maximum cosine angles.
387 return smoothstep( minCos, maxCos, cosAngle );
388 }

First, the cosine of the spotlight's cone is calculated. If the dot product between the spotlight's direction and the light vector (L) is less than the minimum cosine value, then the light contribution will be 0. If the dot product is greater than the maximum cosine angle, then the spotlight's contribution will be 1.

Minimum and maximum cosine angles for spotlights

It may seem counterintuitive that the maximum cosine angle is smaller than the minimum cosine angle, but don't forget that the cosine of 0° is 1 and the cosine of 90° is 0.

The DoSpotLight function will calculate the contribution of a spotlight similarly to the contribution of a point light, plus the cosine angle of the spotlight.

// ForwardRendering.hlsl
418 LightingResult DoSpotLight( Light light, Material mat, float4 V, float4 P, float4 N )
419 {
420 LightingResult result;
421
422 float4 L = light.PositionVS - P;
423 float distance = length( L );
424 L = L / distance;
425 
426 float attenuation = DoAttenuation( light, distance );
427 float spotIntensity = DoSpotCone( light, L );
428 
429 result.Diffuse = DoDiffuse( light, L, N ) * 
430 attenuation * spotIntensity * light.Intensity;
431 result.Specular = DoSpecular( light, mat, V, L, N ) * 
432 attenuation * spotIntensity * light.Intensity;
433 
434 return result;
435 }

2.3.2.6 Directional Lights

Directional lights are the simplest type of light because they do not fall off at the point being shaded.

// ForwardRendering.hlsl
406 LightingResult DoDirectionalLight( Light light, Material mat, float4 V, float4 P, float4 N )
407 {
408 LightingResult result;
409
410 float4 L = normalize( -light.DirectionVS );
411 
412 result.Diffuse = DoDiffuse( light, L, N ) * light.Intensity;
413 result.Specular = DoSpecular( light, mat, V, L, N ) * light.Intensity;
414 
415 return result;
416 }

2.3.2.7 Final coloring

Now that we have the material properties and overlay lighting effects for all the lights in the scene, we can combine them for the final shading.

// ForwardRendering.hlsl
111 float4 P = float4( IN.positionVS, 1 );
112
113 LightingResult lit = DoLighting( Lights, mat, eyePos, P, N );
114 
115 diffuse *= float4( lit.Diffuse.rgb, 1.0f ); // Discard the alpha value from the lighting calculations.
116 
117 float4 specular = 0;
118 if ( mat.SpecularPower > 1.0f ) // If specular power is too low, don't use it.
119 {
120 specular = mat.SpecularColor;
121 if ( mat.HasSpecularTexture )
122 {
123 float4 specularTex = SpecularTexture.Sample( LinearRepeatSampler, IN.texCoord );
124 if ( any( specular.rgb ) )
125 {
126 specular *= specularTex;
127 }
128 else
129 {
130 specular = specularTex;
131 }
132 }
133 specular *= lit.Specular;
134 }
135 
136 return float4( ( ambient + emissive + diffuse + specular ).rgb, 
137 alpha * mat.Opacity );
138 
139 }

On line 113, the lighting contribution is calculated using the DoLighting function just described.

In line 115, the material's diffuse color is adjusted by the light's diffuse contribution.

If a material's specular intensity is below 1.0, it will not be considered for final shading. If the material has no specular, some artists specify a specular intensity less than 1. In this case, we simply ignore the specular contribution and the material is considered diffuse-only (lambert reflection). Otherwise, if the material has a specular texture associated with it, it will be sampled and combined with the material's specular color, which is then modulated with the light's specular contribution.

The final pixel color is the sum of the ambient, emissive, diffuse and specular colors, and the pixel's opacity is determined by the alpha value previously determined in the pixel shader.

Game Rendering Technology: Forward Rendering vs Delayed Rendering vs Forward+ Rendering (1)_Kaitiren's Blog-CSDN Blog

To be continued. . .

Guess you like

Origin blog.csdn.net/Kaitiren/article/details/131639496