Performant SSS (Sub-Surface Scattering) Approximation

An approximation of sub-surface scattering for real-time environments.
Works well with every rendering implemnetation (Forward+, Mobile, Compatibility)

Setup: (Blender required)

  • Open your model in Blender.
  • See screenshot 1 for SSS map baking setup.
  • Apply a ShaderMaterial with this shader to a MeshInstance3D.
  • Fill out all required uniform fields.
Shader code
/*
Sub-Surface Scattering Approximation
Copyright (c) 2024 Ivan Reshetnikov

Permission is hereby granted, free of charge, to any person obtaining a copy of
this shader and associated documentation files (the "Shader"), to deal in the
Shader without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
Shader, and to permit persons to whom the Shader is furnished to do so, subject
to the following conditions:

THE SHADER IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SHADER OR THE USE OR OTHER DEALINGS IN THE SHADER.
*/
/*
Godot Standard Light Shader (Replica) by RustyRoboticsBV
https://github.com/RustyRoboticsBV/GodotStandardLightShader
*/

/*
Parts of this shader were automatically converted
from Godot Engine 4.2.stable's StandardMaterial3D.
*/

shader_type spatial;
render_mode blend_mix, depth_draw_opaque, diffuse_burley, specular_schlick_ggx;

uniform vec4 albedo : source_color;
uniform sampler2D texture_albedo : source_color, filter_linear_mipmap, repeat_enable;

uniform vec4 metallic_texture_channel;
uniform float metallic;
uniform sampler2D texture_metallic : hint_default_white,filter_linear_mipmap,repeat_enable;

uniform float roughness : hint_range(0, 1);
uniform sampler2D texture_roughness : hint_roughness_g,filter_linear_mipmap,repeat_enable;

uniform float specular;
uniform float normal_scale : hint_range(-16,16);
uniform sampler2D texture_normal : hint_roughness_normal,filter_linear_mipmap,repeat_enable;

uniform float subsurface_scattering_factor = 4.0;
uniform sampler2D subsurface_scattering_map : filter_linear_mipmap, repeat_enable;


void fragment() {
	vec2 base_uv = UV;
	vec4 albedo_tex = texture(texture_albedo,base_uv);
	ALBEDO = albedo.rgb * albedo_tex.rgb;
	float metallic_tex = dot(texture(texture_metallic,base_uv),metallic_texture_channel);
	METALLIC = metallic_tex * metallic;
	vec4 roughness_texture_channel = vec4(0.0,1.0,0.0,0.0);
	float roughness_tex = dot(texture(texture_roughness,base_uv),roughness_texture_channel);
	ROUGHNESS = roughness_tex * roughness;
	SPECULAR = specular;
	NORMAL_MAP = texture(texture_normal,base_uv).rgb;
	NORMAL_MAP_DEPTH = normal_scale;
}

/* Start of:
Godot Standard Light Shader (Replica) by RustyRoboticsBV
https://github.com/RustyRoboticsBV/GodotStandardLightShader
*/
float DistributionGGX(float cos_theta_m, float alpha)
{
	float alpha2 = alpha * alpha;
	float d = 1.0 + (alpha2 - 1.0) * cos_theta_m * cos_theta_m;
	return alpha2 / (PI * d * d);
}

float GeometryGGX(float NdotL, float NdotV, float alpha)
{
	return 0.5 / mix(2.0 * NdotL * NdotV, NdotL + NdotV, alpha);
}

vec3 SchlickBaseReflectivity(float metallic_, float specular_, vec3 albedo_)
{
	float dielectric = 0.04 * specular_ * specular;
	return mix(vec3(dielectric), albedo_, vec3(metallic_));
}
	
float SchlickFresnel(float u)
{
	float m = 1.0 - u;
	float m2 = m * m;
	return m2 * m2 * m;
}
/* End of:
* Godot Standard Light Shader (Replica) by RustyRoboticsBV
* https://github.com/RustyRoboticsBV/GodotStandardLightShader
*/

void light() {
	// Diffuse light
	DIFFUSE_LIGHT += max(dot(NORMAL, LIGHT), 0.0) * LIGHT_COLOR;
	
	// Sub-surface scattering
	float sss_map_factor = texture(subsurface_scattering_map, UV).r;
	
	SPECULAR_LIGHT += 
		max(abs(dot(-NORMAL, LIGHT)), 0.0) // Directional light factor
		* LIGHT_COLOR / PI // Normalized light color
		* ALBEDO
		* sss_map_factor * sss_map_factor // Channel transmission with SSS map
		* subsurface_scattering_factor;
	
/* Start of:
Godot Standard Light Shader (Replica) by RustyRoboticsBV
https://github.com/RustyRoboticsBV/GodotStandardLightShader
*/
	// Calculate some vectors.
	vec3 lightColor = LIGHT_COLOR / PI;
	
	vec3 half = normalize(VIEW + LIGHT);
	
	float NdotL = max(dot(NORMAL, LIGHT), 0.0);
	float NdotV = max(dot(NORMAL, VIEW), 0.0);
	float NdotH = max(dot(NORMAL, half), 0.0);
	float LdotH = max(dot(LIGHT, half), 0.0);
	
	// Specular light (Schlick-GGX).
	float ggxAlpha = ROUGHNESS * ROUGHNESS;
	float D = DistributionGGX(NdotH, ggxAlpha);
	float G = GeometryGGX(NdotL, NdotV, ggxAlpha);
	
	vec3 f0 = SchlickBaseReflectivity(METALLIC, SPECULAR_AMOUNT, ALBEDO);
	float LdotH5 = SchlickFresnel(LdotH);
	float f90 = clamp(50.0 * f0.g, 0.0, 1.0);
	vec3 F = f0 + (f90 - f0) * LdotH5;
	
	vec3 specularBRDF = max(NdotL * D * G * F, 0.0);
	SPECULAR_LIGHT += specularBRDF * LIGHT_COLOR * ATTENUATION;
/* End of:
* Godot Standard Light Shader (Replica) by RustyRoboticsBV
* https://github.com/RustyRoboticsBV/GodotStandardLightShader
*/
}
Tags
cheap, flora, foliage, SSS, sub surface scattering
The shader code and all code snippets in this post are under MIT license and can be used freely. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments