Illumination in [Unity Shaders]Shader

Posted by freeloader on Wed, 15 May 2019 20:35:40 +0200

Rendering details of Forward Rendering Path

 

Before we start the discussion later, we need to understand what Unity can handle in Forward Rendering Path and how much light it can handle. Here only extract Official documents Some of them are explained.

 

In Forward Rendering, there are three ways to deal with illumination: vertex-by-vertex processing, pixel-by-pixel processing, and Spherical Harmonics (SH) processing. And deciding which processing mode a light is depends on its type and mode:

 

  • The brightest parallel light in the scene is always processed pixel by pixel. This means that if there is only one parallel light in the scene, it doesn't matter whether to set its mode or not.
  • Render Mode is set to Not Important light source, which is processed by vertex-by-vertex or spherical harmonic function. Experiments show that the parallel light in the first point is not constrained by this point.
  • Render Mode is set to an Important light source, which is processed pixel by pixel.
  • If the number of pixel light sources obtained according to the above rules is less than the number of pixel light Counts in the setting, in order to reduce the brightness, more light sources will render in a pixel-by-pixel manner.
    • I didn't understand that. My experimental results show that if all the light sources are set to Auto, the number of pixel-by-pixel light sources will not exceed Pixel Light Count. But if Render Mode is set to explicitly Not Important or Important, then setting Pixel Light Count does not seem to have any effect.
       

 

 

Where is the light treatment? It's in Pass, of course. Forward Rendering has two kinds of Pass: Base Pass, Additional Passes. The illustrations of these two Pass are as follows:

 

Note that Per-Vertex Lights/SH Lights are marked as optional, which means that we can choose whether or not to deal with these light sources. If we do not specify the relevant processing functions in Base Pass, these light sources will not actually affect the object. Another point is that the orange character indicates the code, which I will not go into Tags, which is the basic requirement. "Pragagma multi_compile_fwdbase" is a long-term experiment that shows that it's better to write them, which will allow some functions and macros to work correctly. Unfortunately, there's no clear documentation available, so it's better to add them every time. Finally, note that for Forward Rendering, only the first parallel light processed in Bass Pass can have a shadow effect.

 

From the above figure, we already know that since the pixel-by-pixel light source is the most important one, Unity will spend an entire Pass processing it. For vertex-by-vertex/SH light sources, they will be processed in Bass Pass (along with the most important parallel light). No weight is the result. Then Base Pass would say, "When I'm so young, let me do so many things, even if the number of parallel lights is small, and the amount of SH light work is small, but if the vertex light comes to disturb me, I won't do it. No way!" I have to be qualified!" So Unity stipulates that at most four light sources will be processed according to vertex-by-vertex light sources, while the others will only be processed according to SH light sources.

 

It's easy to get confused here. Let's first look at the official situation, that is, the first situation: all light sources are set to Auto. In this case, Unity automatically chooses the right type of light source. At this point, it is important to have a project setup, Pixel Light Count, which determines the maximum number of pixel-by-pixel light. When Pixel Light Count is 4, it's the famous legend (from Official documents):

 

 

The above type selection process is roughly like this: first, the former Pixel Light Count (here 4) light sources are processed pixel by pixel, then up to four vertex by vertex light sources, and the rest is SH light. Among them, note that there will be overlap between each light source, which is mainly to prevent sudden changes in light when objects move.

But what happens if the light source is not set to Auto, but is designated Important and Not Important? (Don't ask me what happens when some of my settings are set to Auto and others are set to Important. You really hate your own analysis.) Then, the second case: customize the light source type. First of all, keep in mind that at this point, instead of being restricted by Pixel Light Count, all Importants are set as pixel-by-pixel light sources and none are left. If not Important is set, up to four light sources will be treated as vertex-by-vertex light sources, and others will be treated as SH light sources.

It sounds very complicated, but in fact it is a process of natural selection. We can imagine that all the light sources are scrambling for more computing resources. They want to be the most important pixel-by-pixel light. If they are really mixed up, they can only be regarded as SH light. So what about earning resources? For pixel-by-pixel light, it has an entire Pass resource to squander, which involves the use of various illumination variables and functions, which will be discussed later; for vertex-by-vertex light and SH light, unfortunately, Unity does not have a clear document to tell us how to access them, we can only use the variable declaration in UnityShaderVariables.cginc and the compiled results of Surface Shader. "Guess" usage. This is what I will talk about later.

Tucao time: Although written in the document, there are still many puzzling questions in the actual process.

  • Singularity 1: In version 4.6.1, I created a scene with one parallel light and four point light sources. If Shader was not defined as Additional Passes, then even if the four point light sources were set as Important, Unity would still be used as vertex-by-vertex light sources.
  • Case 2: If you only define Additional Passes without Base Pass, it's even more wonderful. Pass doesn't feel like it's working, and the result is something like the last cache. In conclusion, you must first define Base Pass and then Additional Passes. Don't be capricious!
  • More exotic flowers await your discovery

Illumination Variables and Functions

 

In the UnityShaderVariables.cginc file, we can find the variables that Unity provides for dealing with illumination:

CBUFFER_START(UnityLighting)


#ifdef USING_DIRECTIONAL_LIGHT

uniform fixed4 _WorldSpaceLightPos0;

#else

uniform float4 _WorldSpaceLightPos0;

#endif


uniform float4 _LightPositionRange; // xyz = pos, w = 1/range


// Built-in uniforms for "vertex lights"

float4 unity_4LightPosX0; // x coordinates of the 4 light sources in world space

float4 unity_4LightPosY0; // y coordinates of the 4 light sources in world space

float4 unity_4LightPosZ0; // z coordinates of the 4 light sources in world space

float4 unity_4LightAtten0; // scale factors for attenuation with squared distance


float4 unity_LightColor[8]; // array of the colors of the 4 light sources

float4 unity_LightPosition[8]; // apparently is not always correctly set

// x = -1

// y = 1

// z = quadratic attenuation

// w = range^2

float4 unity_LightAtten[8]; // apparently is not always correctly set

float4 unity_SpotDirection[8];


// SH lighting environment

float4 unity_SHAr;

float4 unity_SHAg;

float4 unity_SHAb;

float4 unity_SHBr;

float4 unity_SHBg;

float4 unity_SHBb;

float4 unity_SHC;

CBUFFER_END

In UnityCG.cginc, we can find the auxiliary function of illumination processing:

// Computes world space light direction

inline float3 WorldSpaceLightDir( in float4 v );


// Computes object space light direction

inline float3 ObjSpaceLightDir( in float4 v );


// Computes world space view direction

inline float3 WorldSpaceViewDir( in float4 v );


// Computes object space view direction

inline float3 ObjSpaceViewDir( in float4 v );


float3 Shade4PointLights (

float4 lightPosX, float4 lightPosY, float4 lightPosZ,

float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,

float4 lightAttenSq,

float3 pos, float3 normal);


float3 ShadeVertexLights (float4 vertex, float3 normal);


// normal should be normalized, w=1.0

half3 ShadeSH9 (half4 normal);

Let's look at how to use the above variables and functions to handle different types of light in two Pass.

A basic Shader

 

The following discussion is mainly based on the following code, you can first scan, here do not need to look at. It mainly calculates diffuse and high light reflection illumination, and also illustrates the calculation of vertex-by-vertex light source and SH light source.

Shader "Light Test" {

Properties {

_Color ("Color", color) = (1.0,1.0,1.0,1.0)

}

SubShader {

Tags { "RenderType"="Opaque"}


Pass {

Tags { "LightMode"="ForwardBase"} // pass for 4 vertex lights, ambient light & first pixel light (directional light)


CGPROGRAM

// Apparently need to add this declaration

#pragma multi_compile_fwdbase


#pragma vertex vert

#pragma fragment frag


#include "UnityCG.cginc"

#include "Lighting.cginc"

#include "AutoLight.cginc"


uniform float4 _Color;


struct vertexInput {

float4 vertex : POSITION;

float3 normal : NORMAL;

};

struct vertexOutput {

float4 pos : SV_POSITION;

float4 posWorld : TEXCOORD0;

float3 normalDir : TEXCOORD1;

float3 lightDir : TEXCOORD2;

float3 viewDir : TEXCOORD3;

float3 vertexLighting : TEXCOORD4;

LIGHTING_COORDS(5, 6)

};


vertexOutput vert(vertexInput input) {

vertexOutput output;


output.pos = mul(UNITY_MATRIX_MVP, input.vertex);

output.posWorld = mul(_Object2World, input.vertex);

output.normalDir = normalize(mul(float4(input.normal, 0.0), _World2Object).xyz);

output.lightDir = WorldSpaceLightDir(input.vertex);

output.viewDir = WorldSpaceViewDir(input.vertex);

output.vertexLighting = float3(0.0);


// SH/ambient and vertex lights

#ifdef LIGHTMAP_OFF

float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));

output.vertexLighting = shLight;

#ifdef VERTEXLIGHT_ON

float3 vertexLight = Shade4PointLights (

unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,

unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,

unity_4LightAtten0, output.posWorld, output.normalDir);

output.vertexLighting += vertexLight;

#endif // VERTEXLIGHT_ON

#endif // LIGHTMAP_OFF


// pass lighting information to pixel shader

TRANSFER_VERTEX_TO_FRAGMENT(output);


return output;

}


float4 frag(vertexOutput input):COLOR{

float3 normalDirection = normalize(input.normalDir);

float3 viewDirection = normalize(_WorldSpaceCameraPos - input.posWorld.xyz);

float3 lightDirection;

float attenuation;


if (0.0 == _WorldSpaceLightPos0.w) // directional light?

{

attenuation = 1.0; // no attenuation

lightDirection = normalize(_WorldSpaceLightPos0.xyz);

}

else // point or spot light

{

float3 vertexToLightSource = _WorldSpaceLightPos0.xyz - input.posWorld.xyz;

float distance = length(vertexToLightSource);

attenuation = 1.0 / distance; // linear attenuation

lightDirection = normalize(vertexToLightSource);

}


// LIGHT_ATTENUATION not only compute attenuation, but also shadow infos

// attenuation = LIGHT_ATTENUATION(input);

// Compare to directions computed from vertex

// viewDirection = normalize(input.viewDir);

// lightDirection = normalize(input.lightDir);


// Because SH lights contain ambient, we don't need to add it to the final result

float3 ambientLighting = UNITY_LIGHTMODEL_AMBIENT.xyz;


float3 diffuseReflection = attenuation * _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection)) * 2;


float3 specularReflection;

if (dot(normalDirection, lightDirection) < 0.0) // light source on the wrong side?

{

specularReflection = float3(0.0, 0.0, 0.0); // no specular reflection

}

else // light source on the right side

{

specularReflection = attenuation * _LightColor0.rgb * _Color.rgb * pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), 255);

}


return float4(input.vertexLighting + diffuseReflection + specularReflection, 1.0);

}

ENDCG

}


Pass{

Tags { "LightMode"="ForwardAdd"} // pass for additional light sources

ZWrite Off Blend One One Fog { Color (0,0,0,0) } // additive blending


CGPROGRAM

// Apparently need to add this declaration

#pragma multi_compile_fwdadd


#pragma vertex vert

#pragma fragment frag


#include "UnityCG.cginc"

#include "Lighting.cginc"

#include "AutoLight.cginc"


uniform float4 _Color;


struct vertexInput {

float4 vertex : POSITION;

float3 normal : NORMAL;

};

struct vertexOutput {

float4 pos : SV_POSITION;

float4 posWorld : TEXCOORD0;

float3 normalDir : TEXCOORD1;

float3 lightDir : TEXCOORD2;

float3 viewDir : TEXCOORD3;

LIGHTING_COORDS(4, 5)

};


vertexOutput vert(vertexInput input) {

vertexOutput output;


output.pos = mul(UNITY_MATRIX_MVP, input.vertex);

output.posWorld = mul(_Object2World, input.vertex);

output.normalDir = normalize(mul(float4(input.normal, 0.0), _World2Object).xyz);

output.lightDir = WorldSpaceLightDir(input.vertex);

output.viewDir = WorldSpaceViewDir(input.vertex);


// pass lighting information to pixel shader

vertexInput v = input;

TRANSFER_VERTEX_TO_FRAGMENT(output);


return output;

}


float4 frag(vertexOutput input):COLOR{

float3 normalDirection = normalize(input.normalDir);

float3 viewDirection = normalize(_WorldSpaceCameraPos - input.posWorld.xyz);

float3 lightDirection;

float attenuation;


if (0.0 == _WorldSpaceLightPos0.w) // directional light?

{

attenuation = 1.0; // no attenuation

lightDirection = normalize(_WorldSpaceLightPos0.xyz);

}

else // point or spot light

{

float3 vertexToLightSource = _WorldSpaceLightPos0.xyz - input.posWorld.xyz;

float distance = length(vertexToLightSource);

attenuation = 1.0 / distance; // linear attenuation

lightDirection = normalize(vertexToLightSource);

}


// LIGHT_ATTENUATION not only compute attenuation, but also shadow infos

// attenuation = LIGHT_ATTENUATION(input);

// Compare to directions computed from vertex

// viewDirection = normalize(input.viewDir);

// lightDirection = normalize(input.lightDir);


float3 diffuseReflection = attenuation * _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection)) * 2;


float3 specularReflection;

if (dot(normalDirection, lightDirection) < 0.0) // light source on the wrong side?

{

specularReflection = float3(0.0, 0.0, 0.0); // no specular reflection

}

else // light source on the right side

{

specularReflection = attenuation * _LightColor0.rgb * _Color.rgb * pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), 255);

}


return float4(diffuseReflection + specularReflection, 1.0);

}

ENDCG

}

}

FallBack "Diffuse"

}​​​​​​​

​​​​​​​Base Pass

Recall that in Bass Pass, we can process all three kinds of illumination: the first parallel light as a pixel-by-pixel light, all vertex-by-vertex light, and all other SH light. Another important point is that we have to deal with ambient light, shadows and so on. In short, since Additional Passes can only process pixel-by-pixel light, if you want other lighting effects, you need to process them in Bass Passes.

Ambient light

The ambient light here refers to our Ambient Light value in Edit - > Render Setting. It's easy to get in Shader by accessing the global variable UNITY_LIGHTMODEL_AMBIENT. It's a global variable, so it can be accessed in any Pass, but the ambient light only needs to be added once, so we just need to superimpose it on other colors in Bass Pass.

Shadow and light attenuation

Base Pass also plays a very important role in adding shadows. As mentioned above, for Forward Rendering, only the first parallel light processed in Bass Pass can have a shadow effect. That is to say, if you miss it, you won't get shadow information. The simulation shadows in the program mainly rely on a Shadow Map, which records the depth information closest to the light source. Unity provides such a texture very intimately (_ShadowMapTexture), which is implemented without our own reprogramming.

Similar to the shadow implementation, Unity also provides a texture (_Light Texture 0), which contains attenuation.

Since both shadows and light attenuation sample textures and multiply the results by color values, Unity combines these two steps into a macro. Let's solve these two problems through a macro call. Since the texture is sampled, the texture coordinates corresponding to the vertex should be known first. Unity is also assisted by macros. We only need to add macro LIGHTING_COORDS in v2f (vertex Output). Then, in order to calculate the coordinates of two textures corresponding to vertices, a new macro, TRANSFER_VERTEX_TO_FRAGMENT, needs to be called in the vert function.

The macro definitions used in this process are in the AutoLight.cginc file.

A complete process is as follows:

First, we must declare Pass and # pragma, so that Unity can fill textures and coordinates correctly:
Forward Add Pass is similar.

Tags { "LightMode"="ForwardBase"} // pass for 4 vertex lights, ambient light & first pixel light (directional light)


CGPROGRAM

// Apparently need to add this declaration

#pragma multi_compile_fwdbase

Define the texture coordinates of illumination texture and shadow texture:
That is, the last line above, LIGHTING_COORDS(5, 6). 5 and 6 indicate the storage location of variables. The definition of this macro varies depending on the type of light source and whether cookie s exist or not.

struct vertexOutput {

float4 pos : SV_POSITION;

float4 posWorld : TEXCOORD0;

float3 normalDir : TEXCOORD1;

float3 lightDir : TEXCOORD2;

float3 viewDir : TEXCOORD3;

float3 vertexLighting : TEXCOORD4;

LIGHTING_COORDS(5, 6)

};

The above code comes from a section in Forward Add. Note that the above redefined structure v is because in TRANSFER_VERTEX_TO_FRAGMENT, if the light source is not parallel light, it is necessary to use the vertex position to calculate attenuation (parallel light does not need to calculate attenuation), and vertex access is directly used in the macro, which means that we must provide a vertex data structure named v in the context. Of course, we can directly change the name of vertexInput to v instead of having to do so...

Then, the correct texture coordinates need to be calculated in the vert function:

vertexOutput vert(vertexInput input) {

vertexOutput output;


......


// pass lighting information to pixel shader

vertexInput v = input;

TRANSFER_VERTEX_TO_FRAGMENT(output);


return output;

}

Finally, we request a shadow or attenuation value in the frag function:

attenuation = LIGHT_ATTENUATION(input);

Unity uses these three macros to calculate shadows and attenuations. Let's see what these three macros are. Here's just an example of parallel light and point light without cookie s on:

#ifdef POINT

#define LIGHTING_COORDS(idx1,idx2) float3 _LightCoord : TEXCOORD##idx1; SHADOW_COORDS(idx2)

uniform sampler2D _LightTexture0;

uniform float4x4 _LightMatrix0;

#define TRANSFER_VERTEX_TO_FRAGMENT(a) a._LightCoord = mul(_LightMatrix0, mul(_Object2World, v.vertex)).xyz; TRANSFER_SHADOW(a)

#define LIGHT_ATTENUATION(a) (tex2D(_LightTexture0, dot(a._LightCoord,a._LightCoord).rr).UNITY_ATTEN_CHANNEL * SHADOW_ATTENUATION(a))

#endif


#ifdef DIRECTIONAL

#define LIGHTING_COORDS(idx1,idx2) SHADOW_COORDS(idx1)

#define TRANSFER_VERTEX_TO_FRAGMENT(a) TRANSFER_SHADOW(a)

#define LIGHT_ATTENUATION(a) SHADOW_ATTENUATION(a)

#endif

#define SHADOW_COORDS(idx1) float4 _ShadowCoord : TEXCOORD##idx1;

#define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_World2Shadow[0], mul(_Object2World,v.vertex));

#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)

It can be found that for point light sources, two kinds of textures are computed, i.e. light attenuation texture and shadow texture. When attenuation is computed finally, the sampling results of the two textures are multiplied. For parallel light, it is simpler, because the parallel light has no attenuation, so it only needs to calculate the shadow texture.

Again, for Forward Rendering, only the first parallel light processed in Bass Pass can have a shadow effect. For example, the parallel light in the left image below can cast shadows, while the ball in the right image does not produce any shadows even between the light source and the apple.

 

per-vertex lighting

In fact, vertex-by-vertex illumination is a name. Unity stores these so-called "vertex-by-vertex illumination" data in some variables, and we can deal with them in a pixel-by-pixel manner. Of course, in terms of performance, we usually deal with them at the vertex function stage, so we call them vertex-by-vertex illumination.

There are two groups of variables and functions involved in vertex-by-vertex illumination. The groups here are categorized mainly by the variables used by the vertex illumination calculation function provided by Unity.

The first group is as follows:

uniform float4 unity_4LightPosX0; // x coordinates of the 4 light sources in world space

uniform float4 unity_4LightPosY0; // y coordinates of the 4 light sources in world space

uniform float4 unity_4LightPosZ0; // z coordinates of the 4 light sources in world space

uniform float4 unity_4LightAtten0; // scale factors for attenuation with squared distance


The corresponding functions are as follows:

float3 Shade4PointLights (

float4 lightPosX, float4 lightPosY, float4 lightPosZ,

float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,

float4 lightAttenSq,

float3 pos, float3 normal)

{

// to light vectors

float4 toLightX = lightPosX - pos.x;

float4 toLightY = lightPosY - pos.y;

float4 toLightZ = lightPosZ - pos.z;

// squared lengths

float4 lengthSq = 0;

lengthSq += toLightX * toLightX;

lengthSq += toLightY * toLightY;

lengthSq += toLightZ * toLightZ;

// NdotL

float4 ndotl = 0;

ndotl += toLightX * normal.x;

ndotl += toLightY * normal.y;

ndotl += toLightZ * normal.z;

// correct NdotL

float4 corr = rsqrt(lengthSq);

ndotl = max (float4(0,0,0,0), ndotl * corr);

// attenuation

float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);

float4 diff = ndotl * atten;

// final color

float3 col = 0;

col += lightColor0 * diff.x;

col += lightColor1 * diff.y;

col += lightColor2 * diff.z;

col += lightColor3 * diff.w;

return col;

}

The code for the call is as follows:

float3 vertexLight = Shade4PointLights (

unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,

unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,

unity_4LightAtten0, output.posWorld, output.normalDir);

Note that both vertex position and normal direction are in the world coordinate system.
The second set of variables:

float4 unity_LightPosition[8]; // apparently is not always correctly set

// x = -1

// y = 1

// z = quadratic attenuation

// w = range^2

float4 unity_LightAtten[8]; // apparently is not always correctly set

float4 unity_SpotDirection[8];

Function:

float3 ShadeVertexLights (float4 vertex, float3 normal)

{

float3 viewpos = mul (UNITY_MATRIX_MV, vertex).xyz;

float3 viewN = mul ((float3x3)UNITY_MATRIX_IT_MV, normal);

float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;

for (int i = 0; i < 4; i++) {

float3 toLight = unity_LightPosition[i].xyz - viewpos.xyz * unity_LightPosition[i].w;

float lengthSq = dot(toLight, toLight);

float atten = 1.0 / (1.0 + lengthSq * unity_LightAtten[i].z);

float diff = max (0, dot (viewN, normalize(toLight)));

lightColor += unity_LightColor[i].rgb * (diff * atten);

}

return lightColor;

}

Usage:

vertexLight = ShadeVertexLights(input.vertex, input.normal)


Note that the vertex coordinates and normal directions are in the object coordinate system. Moreover, the calculation results include ambient light...

These two sets of functions seem to do the same job, but in fact, in Forward Rendering, we can only choose the first set. Below is Official documents Interpretation in:

Forward rendering helper functions in UnityCG.cginc

These functions are only useful when using forward rendering (ForwardBase or ForwardAdd pass types).

  • float3 Shade4PointLights (...) - computes illumination from four point lights, with light data tightly packed into vectors. Forward rendering uses this to compute per-vertex lighting.

Vertex-lit helper functions in UnityCG.cginc

These functions are only useful when using per-vertex lit shaders ("Vertex" pass type).

  • float3 ShadeVertexLights (float4 vertex, float3 normal) - computes illumination from four per-vertex lights and ambient, given object space position & normal.

It is clear from the document that for Forward Rendering, we should use Shade4Point Lights to calculate up to four vertex-by-vertex illuminations, and only Point Lights and Spot Lights can be calculated. If a parallel light is set to vertex-by-vertex illumination, it will not be calculated. In other words, we should use unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, unity_4LightAtten0 to access vertex-by-vertex light source data. The other group is used in Vertex Pass (e.g. Tags {"LightMode"="Vertex"}.

There are still some things we need to know about:

  • Unity's function is just for the convenience of a calculation method. It can be seen that in Shade4Point Lights, the illumination in diffuse reflection direction is calculated by vertex-by-vertex method (that is, only vertex position and normal line are provided in vert function), but we can also deal with vertex-by-vertex light source according to these illumination variables, such as adding highlight reflection, etc.
  • We can even process them in a pixel-by-pixel fashion, accessing and calculating them in frag functions. Nothing can stop you from doing so if you like. (That's how wayward.)

Well, after that, let's see what the visual effect is. We put a small apple + a ball in the scene, and put four different color point light sources, output only Shade4Point Lights as follows (the left image is vertex-by-vertex illumination, the right image is pixel-by-pixel illumination):

 

It can be seen that the vertex-by-vertex light source is not as good as the pixel-by-pixel light source in visual effect, but its performance is better.

Then, there is another problem, that is, the number of vertex-by-vertex light sources supporting calculation is up to four, and the defined array of variables storing vertex-by-vertex light sources information is only four dimensions. In other words, if the number of vertex-by-vertex light sources in the scene is set (or sorted) to be more than four, Unity will sort them and store the four most important light sources in those variables. However, the sorting method Unity has no document to explain, and from the experimental results, the sorting results are related to the color, density and distance of light. For example, if we add a blue light source, we can see that there will be no change in the results:

If we adjust its color, density, or position, a sudden change in illumination will occur due to the change of the sorting result (left picture is changed color, right picture is changed density):

  

SH illumination

Those light sources are neither pixel-by-pixel light nor vertex-by-vertex light. If they want to affect objects, they can only be processed according to SH light. This is the result of the failure of the palace battle. In Unity, the variables and functions related to the calculation of SH light are as follows:

// SH lighting environment

float4 unity_SHAr;

float4 unity_SHAg;

float4 unity_SHAb;

float4 unity_SHBr;

float4 unity_SHBg;

float4 unity_SHBb;

float4 unity_SHC;​​​​​​​
// normal should be normalized, w=1.0

half3 ShadeSH9 (half4 normal)

{

half3 x1, x2, x3;


// Linear + constant polynomial terms

x1.r = dot(unity_SHAr,normal);

x1.g = dot(unity_SHAg,normal);

x1.b = dot(unity_SHAb,normal);


// 4 of the quadratic polynomials

half4 vB = normal.xyzz * normal.yzzx;

x2.r = dot(unity_SHBr,vB);

x2.g = dot(unity_SHBg,vB);

x2.b = dot(unity_SHBb,vB);


// Final quadratic polynomial

float vC = normal.x*normal.x - normal.y*normal.y;

x3 = unity_SHC.rgb * vC;

return x1 + x2 + x3;

}

The calling code is as follows:

float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));

I have not studied the implementation details of SH illumination. Interested people can look up the data to understand the meaning of the above function. I was told by a netizen's message before An article . But it's too long for me to watch... And in the Forum A post See the code inside for a preliminary understanding.

Let's take the previous example and see the result of exporting SH light only. In the left image below, there are only four light sources, so we can see that there is no SH light at this time, because these four light sources are treated as vertex-by-vertex illumination at this time. The color of the object is not black because unity_SHAr, unity_SHAg and unity_SHAb contain ambient light data, but not real light. So in theory, as long as they contain the code for calculating SH light, there is no need to add the ambient light mentioned above to the final result. The right image shows the SH illumination results after adding four new Not Important light sources.

 

We combine vertex-by-vertex illumination with SH illumination. The code is as follows:

// SH/ambient and vertex lights

#ifdef LIGHTMAP_OFF

float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));

output.vertexLighting = shLight;

#ifdef VERTEXLIGHT_ON

float3 vertexLight = Shade4PointLights (

unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,

unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,

unity_4LightAtten0, output.posWorld, output.normalDir);

output.vertexLighting += vertexLight;

#endif // VERTEXLIGHT_ON

#endif // LIGHTMAP_OFF

Among them, # ifdef declarations need to be added to ensure that these illuminations are not counted when Unity does not provide these data.

By adding the two together, we can get the following results:

Additional Passes

Finally, let's talk about pixel-by-pixel light in Additional Passes. What we need to know is that in Base Pass we also need to process pixel-by-pixel light, but we can clearly know that this pixel-by-pixel light can only be the first parallel light. In Additional Passes, the pixel-by-pixel light may be a parallel light source, a point light source, and a spotlight source. This does not discuss the use of LightMap or the opening of Cookie.

Similarly, the pixel-by-pixel light here is actually just a name, Unity is only responsible for putting the so-called pixel-by-pixel light data into some variables, but nothing can prevent us from computing in vert or frag.

Note: If you want Additional Passes to be superimposed on Bass Pass es (the general purpose is this), make sure you add the appropriate mixing mode to Pass. For example:

Pass{

Tags { "LightMode"="ForwardAdd"} // pass for additional light sources

ZWrite Off Blend One One Fog { Color (0,0,0,0) } // additive blending​​​​​​​

For pixel-by-pixel illumination, the variables and functions we use most are as follows:

From UnityShaderVariables.cginc:

uniform float4 _WorldSpaceLightPos0;

uniform float3 _WorldSpaceCameraPos;

From Lighting.cginc:

fixed4 _LightColor0;

From UnityCG.cginc( Document description): ​​​​​​​

// Computes world space light direction

inline float3 WorldSpaceLightDir( in float4 v );

// Computes object space light direction

inline float3 ObjSpaceLightDir( in float4 v );

// Computes world space view direction

inline float3 WorldSpaceViewDir( in float4 v );

// Computes object space view direction

inline float3 ObjSpaceViewDir( in float4 v );

It can be found that only functions give explicit documentation, and the rest can only be guessed by the structure of Shader inside Unity.

Regardless of these variables and functions, let's first think about what we want to use pixel-by-pixel light to calculate and where. The most common requirement is to calculate the direction of the light source and the angle of view, and then calculate the diffuse and high-light reflections. Where Unity calculates these directions doesn't seem to make much difference visually. In theory, it calculates faster in vert than in frag. But the choice of location determines how we can use the above variables and functions.

It can be noted that the functions provided by Unity are all auxiliary functions in vert function, that is, only the vertex position is needed to obtain the direction of illumination and the direction of view. That is to say, if we want to compute values in all directions in vert function, we can do this:

output.lightDir = WorldSpaceLightDir(input.vertex);

output.viewDir = WorldSpaceViewDir(input.vertex);

Of course, the above is to get the use of the world coordinate system, we can also get the object coordinate system, depending on the demand. These functions actually use _WorldSpace LightPos0 and _WorldSpace CameraPos. For example, WorldSpace LightDir is defined as follows:

// Computes world space light direction

inline float3 WorldSpaceLightDir( in float4 v )

{

float3 worldPos = mul(_Object2World, v).xyz;

#ifndef USING_LIGHT_MULTI_COMPILE

return _WorldSpaceLightPos0.xyz - worldPos * _WorldSpaceLightPos0.w;

#else

#ifndef USING_DIRECTIONAL_LIGHT

return _WorldSpaceLightPos0.xyz - worldPos;

#else

return _WorldSpaceLightPos0.xyz;

#endif

#endif

}

Since the direction of the parallel light does not change with the vertex position, it is possible to use _WorldSpace LightPos0.xyz directly. In this case, the direction of the parallel light is actually stored, not the position. At the same time, _WorldSpace LightPos0.w can indicate the type of light source. If 0 is a parallel light source, 1 is a point light source or a spotlight source. As a result, we often see code similar to the following:

if (0.0 == _WorldSpaceLightPos0.w) // directional light?

{

attenuation = 1.0; // no attenuation

lightDirection = normalize(_WorldSpaceLightPos0.xyz);

}

else // point or spot light

{

float3 vertexToLightSource = _WorldSpaceLightPos0.xyz - input.posWorld.xyz;

lightDirection = normalize(vertexToLightSource);

}

In fact, it means the same thing as the WorldSpace LightDir function.

_ LightColor0 has nothing to say. It stores the color of the pixel-by-pixel light.

Topics: Unity REST Fragment less