- Home /
How do I incorporate per-vertex lighting data into a surface shader?
Context
I am working on a game for modern PCs but which has stylized graphics somewhat reminiscent of older 3D games. One goal for my team is to perform rendering which computes lighting on a per-vertex basis, but which leverages Unity's Global Illumination, baked lights, etc. I want to write this as a surface shader, and as a starting point, I've written one which exposes most of the opportunities for customization. There's custom per-vertex data, a custom lighting model, and a custom function to return the final color of a shaded surface.
https://hastebin.com/cakodidinu.cs
One way to accomplish my visual goal might be to compute the UnityGI
value on a per-vertex basis, pass that into the Lighting Model, and use these per-vertex GI values in lieu of the per-pixel values provided. This would create a very artifact-heavy look which varies depending on how detailed the geometry is. That's something that would be good for this game.
struct custom_per_vert_data {
// Other fields like UVs
UnityGI vertexInterpolatedGI;
};
struct custom_surface_output { /* Albedo, Normal, etc */ };
void CustomVert(inout custom_input_data v, out custom_per_vert_data o)
{
// Assign other fields
o.vertexInterpolatedGI = GET_GI_AT_VERTEX(v);
}
void CustomSurface(custom_per_vert_data v, inout custom_surface_output o)
{
o = GET_SURFACE_PROPERTIES(/* UVs and such from v */);
}
half4 LightingCustomModel(custom_surface_output s, custom_per_vert_data v, float3 viewDir, UnityGI gi)
{
// Ignore gi param as it is not per-vertex
return PerformSomewhatStandardLighting(s, viewDir, v.vertexInterpolatedGI);
}
However, this doesn't work because the function signature for custom lighting models cannot have the custom_per_vert_data
parameter. I'll have to use a less elegant solution instead.
Question
What is the best way to incorporate per-vertex lighting data into a surface shader? Something as close to the above "ideal" solution would be nice. I have identified two options, though there may be others.
The first solution is to transport the per-vertex lighting data to the lighting model function via fields in the custom_surface_output
struct.
struct small_light_properties { /* Color, maybe one other field */ }
struct custom_per_vert_data {
// Other fields like UVs
small_light_properties vertexLight;
};
struct surface_properties { /*Albedo, Normal, Etc*/ }
struct custom_surface_output
{
surface_properties surfaceProperties;
UnityGI vertexLight;
};
void CustomVert(inout custom_input_data v, out custom_per_vert_data o)
{
// Assign other fields like UVs
o.vertexLight = GET_GI_AT_VERTEX(v);
}
void CustomSurface(custom_per_vert_data v, inout custom_surface_output o)
{
o.surfaceProperties = GET_SURFACE_PROPERTIES(/* UVs and such from v */);
o.vertexLight = v.vertexLight;
}
half4 LightingCustomModel(custom_surface_output s, float3 viewDir, UnityGI gi)
{
// gi paramter is per-pixel and is ignored
return PerformLessStandardLighting(s.surfaceProperties, viewDir, s.vertexLight);
}
This isn't great because it makes custom_surface_output
-- a struct which is ostensibly just for surface-related data -- also carry lighting-related data. I'm not sure what the consequences of this would be. Additionally, the number of interpolators allowed in custom_surface_output
is limited by Shader Model 3.0 -- this means I can't have all the GI-related data in it.
Another solution is the compute the lighting in the CustomColor
function, which executes last. The lighting model then only serves to pass forward the view direction.
struct small_light_properties { /* Color, maybe one other field */ }
struct custom_per_vert_data {
// Other fields like UVs
small_light_properties vertexLight;
};
struct surface_properties { /*Albedo, Normal, Etc*/ }
struct custom_surface_output
{
surface_properties surfaceProperties;
};
void CustomVert(inout custom_input_data v, out custom_per_vert_data o)
{
// Assign other fields like UVs
o.vertexLight = GET_LIGHT_AT_VERT(v);
}
void CustomSurface(custom_per_vert_data v, inout custom_surface_output o)
{
o.surfaceProperties = GET_SURFACE_PROPERTIES(/* UVs and such from v */);
}
half4 LightingCustomModel(custom_surface_output s, float3 viewDir, UnityGI gi)
{
return viewDir;
}
void CustomColor(custom_per_vert_data v, custom_surface_output o, inout fixed4 colorFromLightingModel)
{
half4 viewDir = colorFromLightingModel;
color = PerformLessStandardLighting(s.surface_properties, viewDir, v.vertexLight);
}
The downsides to this is that it makes it so that the lighting model is totally phony... again I'm not sure what the ramifications of this are.
Anyway, I'm interested to hear which of these solutions is better, and if there's another one I haven't thought of.