Subsurface scattering

I was wondering how I’d go about implementing subsurface scattering with PlayCanvas. I have got a ‘Scattering’ texture map but I don’t know what to do with it. Does this have to be a custom shader? Or perhaps there is already an implementation avaliable?

From what I am aware of I haven’t seen any Playcanvas implementation of a subsurface scattering algorithm. It will have to definitely be a custom shader.

Sketchfab provides one in their model viewer, to check it for reference and you may find some examples in Shadertoy to study source code and how they are implemented.

So I’ve been experimenting with the shader programming… mainly trying to get an existing one to work.
src: https://machinesdontcare.wordpress.com/2008/10/29/subsurface-scatter-shader/

My new code:
Vertex Shader:

////////////////////////////
// SUB-SURFACE SCATTER VS //
////////////////////////////
 
/* --------------------------
SubScatter Vertex Shader:
 
Fake sub-surface scatter lighting shader by InvalidPointer 2008.
Found at
http://www.gamedev.net/community/forums/topic.asp?topic_id=481494
 
HLSL > GLSL translation
toneburst 2008
-------------------------- */

attribute vec2 aUv0;
attribute vec3 aNormal;
attribute vec4 aPosition;

uniform mat4 matrix_viewProjection;
uniform mat4 matrix_model;
// uniform mat4 matrix_view;
uniform mat3 matrix_normal;
// uniform vec3 uLightPos;

varying vec2 texCoord;

varying vec2 vUv0;
// Set light-position
uniform vec3 uLightPos;

// Varying variables to be sent to Fragment Shader
varying vec3 worldNormal, eyeVec, lightVec, vertPos, lightPos;
 
void subScatterVS(in vec4 ecVert)
{
    lightVec = uLightPos - ecVert.xyz;
    eyeVec = -ecVert.xyz;
    vertPos = ecVert.xyz;
    lightPos = uLightPos;
}
 
////////////////
//  MAIN LOOP //
////////////////
 
void main()
{
    vUv0 = aUv0;
    worldNormal = matrix_normal * aNormal;
     
    vec4 ecPos = matrix_viewProjection * matrix_model * aPosition;
     
    // Call function to set varyings for subscatter FS
    subScatterVS(ecPos);
     
    //Transform vertex by modelview and projection matrices
    gl_Position = ecPos;
 
    //Forward current texture coordinates after applying texture matrix
    //gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;
}

Fragment Shader:

////////////////////////////
// SUB-SURFACE SCATTER FS //
////////////////////////////
 
/* --------------------------
SubScatter Fragment Shader:
 
Fake sub-surface scatter lighting shader by InvalidPointer 2008.
Found at
http://www.gamedev.net/community/forums/topic.asp?topic_id=481494

HLSL > GLSL translation
toneburst 2008
-------------------------- */
varying vec2 vUv0;
// Variables for lighting properties
uniform float MaterialThickness;
uniform vec3 ExtinctionCoefficient; // Will show as X Y and Z ports in QC, but actually represent RGB values.
uniform vec4 LightColor;
uniform vec4 BaseColor;
uniform vec4 SpecColor;
uniform float SpecPower;
uniform float RimScalar;
uniform sampler2D Texture;
 
// Varying variables to be sent to Fragment Shader
varying vec3 worldNormal, eyeVec, lightVec, vertPos, lightPos;
 
float halfLambert(in vec3 vect1, in vec3 vect2)
{
    float product = dot(vect1,vect2);
    return product * 0.5 + 0.5;
}
 
float blinnPhongSpecular(in vec3 normalVec, in vec3 lightVec, in float specPower)
{
    vec3 halfAngle = normalize(normalVec + lightVec);
    return pow(clamp(0.0, 1.0,dot(normalVec,halfAngle)),specPower);
}
 
// Main fake sub-surface scatter lighting function
 
vec4 subScatterFS()
{
    float attenuation = 10.0 * (1.0 / distance(lightPos, vertPos));
    vec3 eVec = normalize(eyeVec);
    vec3 lVec = normalize(lightVec);
    vec3 wNorm = normalize(worldNormal);
     
    vec4 dotLN = vec4(halfLambert(lVec, wNorm) * attenuation);
    dotLN *= texture2D(Texture, vUv0);
    //dotLN *= vec4(1.0, 1.0, 1.0, 1.0);//BaseColor;
     
    vec3 indirectLightComponent = vec3(.2 * max(0.0, dot(-wNorm, lVec)));
    indirectLightComponent += .2 * halfLambert(-eVec, lVec);
    indirectLightComponent *= attenuation;
    indirectLightComponent.r *= ExtinctionCoefficient.r;
    indirectLightComponent.g *= ExtinctionCoefficient.g;
    indirectLightComponent.b *= ExtinctionCoefficient.b;
    
    vec3 rim = vec3(1.0 - max(0.0,dot(wNorm, eVec)));
    rim *= rim;
    rim *= max(0.0, dot(wNorm,lVec)) * SpecColor.rgb;
     
    vec4 finalCol = dotLN + vec4(indirectLightComponent, 1.0);
    finalCol.rgb += (rim * RimScalar * attenuation * finalCol.a);
    finalCol.rgb += vec3(blinnPhongSpecular(wNorm,lVec, SpecPower) * attenuation * SpecColor * finalCol.a * 0.05);
    finalCol.rgb *= LightColor.rgb;
     
    return finalCol;
    //return vec4(1.0, 0.0, 1.0, 1.0);
}
 
////////////////
//  MAIN LOOP //
////////////////
 
void main()
{
    gl_FragColor = subScatterFS();
}

CustomShader.js

CustomShader.prototype.initialize = function() {
    this.time = 0;

    var app = this.app;
    var model = this.entity.model.model;
    var gd = app.graphicsDevice;

    var diffuseTexture = this.diffuseMap.resource;

    var vertexShader = this.vs.resource;
    var fragmentShader = "precision " + gd.precision + " float;\n";
    fragmentShader = fragmentShader + this.fs.resource;

    // A shader definition used to create a new shader.
    var shaderDefinition = {
        attributes: {
            aPosition: pc.SEMANTIC_POSITION,
            aUv0: pc.SEMANTIC_TEXCOORD0,
            aNormal: pc.SEMANTIC_NORMAL
        },
        vshader: vertexShader,
        fshader: fragmentShader
    };

    // Create the shader from the definition
    this.shader = new pc.Shader(gd, shaderDefinition);

    // Create a new material and set the shader
    this.material = new pc.Material();
    this.material.shader = this.shader;
    
    var pos = this.light.getPosition();
    this.material.setParameter("uLightPos", new Float32Array([pos.x, pos.y, pos.z]));
    
    this.material.setParameter("Texture", this.diffuseMap.resource);
    
    this.material.setParameter('MaterialThickness', this.thickness);
    
    var ec = this.extinctionCoefficient;
    this.material.setParameter('ExtinctionCoefficient', new Float32Array([ec.x, ec.y, ec.z]));
    
    var lColor = this.light.light.color;
    this.material.setParameter('LightColor', new Float32Array([lColor.r, lColor.g, lColor.b, lColor.a]));
    
    var specColor = this.specColor;
    this.material.setParameter('SpecColor', new Float32Array([specColor.r, specColor.g, specColor.b, specColor.a]));
    
    this.material.setParameter('RimScalar', this.rimScalar);  
    
    // Replace the material on the model with our new material
    model.meshInstances[0].material = this.material;
};

Result

This leaves me with a few questions;
What should gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0; be, and what should it actually do? (I commented it out)
How can I get other lights to light my model? Like the directional light.

gl_MultiTexCoord0 and gl_TextureMatrix aren’t available in WebGL/OpenGL ES2.0 from what I know, not sure how you would rewrite this to calculate tex coords differently.

For lighting/shadows etc the easiest way is to override Playcanvas shader chunks instead of building a shader from scratch. That way you get for free compatibility with existing materials, texture maps, lighting etc. It requires some effort to get it right and learn how to plugin your code, but ultimately it pays off.