How can I add shadows when using ShaderMaterial?

Hi everyone,

I’m trying to figure out how to add shadows when using a custom ShaderMaterial. Here’s a simple example I’m working with:
:backhand_index_pointing_right: https://codepen.io/Tina-Koval/pen/ZYGmNqd

How can I modify my shaders or scene setup to make shadows work correctly with ShaderMaterial?

Code examples would be very helpful—thank you!

So as far as I know ShaderMaterial is unlit—it doesn’t hook into Three.js’s lighting. Out of the box it ignores every light in your scene. If you want it to “see” shadows you must use other materials like MeshLambertMaterial, MeshPhongMaterial, MeshStandardMaterial with lights in the scene, but if you want ShaderMaterial to react to lights, you got two ways,

First way is to get library and use the ShaderMaterial they provide in there, you can find a tutorial here.

Second way is to do the light calculations in the shaders like this but it’s a bit complicated and I pulled one of my old codes and changed some lines and it’s not the best but it’ll give you and idea. It calculates the shadowmap with a renderTarget and the rest I’ll explain in depth if you need more info on how to improve that same code to match your needs.

Also I saw this property they added called lights and I’m not sure how that works. so if someone reading this, they might help you with that part.

1 Like

This site is really useful for seeing how the default materials are constructed.. you can use it to find / understand how things like lights are implemented, and which shader “chunks” you can include for different things..

1 Like

This is not true, you can set a flag on the shader material to use lights. Then do you call the right uniforms, you will get lights. If you inject the chunk, you will get the same computation.

Thanks for the clarification! I did mention the lights property at the end of my post, but I wasn’t sure how it worked in practice. so I appreciate you pointing that out.

I looked into it a bit more, and it turns out that setting ShaderMaterial.lights = true does allow Three.js to inject lighting uniforms and chunks. But you still need to manually include the right chunks and handle the lighting logic in your shader. So it’s not quite “plug and play” like with MeshStandardMaterial, but definitely doable with the right setup.

If you’ve got a working example or a snippet that shows how you’re doing it, We’d love to see it.

Here are the built in materials recreated as shader materials:

In my water shader i used shadows without lights code.
https://codepen.io/illuminsi/pen/ZYGPZwL


		const uniforms = { maskTex: { value: maskTexture } };
		for(let i in THREE.UniformsLib["lights"]){
		uniforms[i]={value:null};
		}
		const material = new THREE.ShaderMaterial({
			uniforms,
			lights:true,
			vertexShader,
			fragmentShader
		});

vertex shader peace:

uniform mat4 directionalShadowMatrix[NUM_DIR_LIGHT_SHADOWS];
varying vec4 vDirectionalShadowCoord[NUM_DIR_LIGHT_SHADOWS];


struct DirectionalLightShadow{
float shadowIntensity;
float shadowBias;
float shadowNormalBias;
float shadowRadius;
vec2 shadowMapSize;
};


uniform DirectionalLightShadow directionalLightShadows[NUM_DIR_LIGHT_SHADOWS];


void main(){
...
for(int i=0;i<NUM_DIR_LIGHT_SHADOWS;i++){
vDirectionalShadowCoord[i]=directionalShadowMatrix[i]*vec4(position,1.0);
}

}

fragment shader peace:

uniform mat4 directionalShadowMatrix[NUM_DIR_LIGHT_SHADOWS];
const float UnpackDownscale=255./256.; // 0..1 -> fraction (excluding 1)
const vec4 PackFactors=vec4(1.0,256.0,256.0*256.0,256.0*256.0*256.0);
const vec4 UnpackFactors4=vec4(UnpackDownscale/PackFactors.rgb,1.0/PackFactors.a);
float unpackRGBAToDepth(const in vec4 v){ return dot(v,UnpackFactors4); }
vec2 unpackRGBATo2Half(const in vec4 v){ return vec2(v.x+(v.y/255.0),v.z+(v.w/255.0)); }


// SHADOWMAP_PARS_FRAGMENT


struct DirectionalLightShadow{
float shadowIntensity;
float shadowBias;
float shadowNormalBias;
float shadowRadius;
vec2 shadowMapSize;
};


uniform DirectionalLightShadow directionalLightShadows[NUM_DIR_LIGHT_SHADOWS];
uniform sampler2D directionalShadowMap[NUM_DIR_LIGHT_SHADOWS];
varying vec4 vDirectionalShadowCoord[NUM_DIR_LIGHT_SHADOWS];


float texture2DCompare(sampler2D depths,vec2 uv,float compare){
return step(compare,unpackRGBAToDepth(texture2D(depths,uv)));


}


vec2 texture2DDistribution(sampler2D shadow,vec2 uv){
return unpackRGBATo2Half(texture2D(shadow,uv));
}


float VSMShadow(sampler2D shadow,vec2 uv,float compare){
float occlusion=1.0;
vec2 distribution=texture2DDistribution(shadow,uv);
float hard_shadow=step(compare,distribution.x); // Hard Shadow
if(hard_shadow!=1.0){
float distance=compare-distribution.x ;
float variance=max(0.00000,distribution.y*distribution.y);
float softness_probability=variance/(variance+distance*distance); // Chebeyshevs inequality
softness_probability=clamp((softness_probability-0.3)/(0.95-0.3),0.0,1.0); // 0.3 reduces light bleed
occlusion=clamp(max(hard_shadow,softness_probability),0.0,1.0);
}
return occlusion;
}


float getShadow(sampler2D shadowMap,vec2 shadowMapSize,float shadowIntensity,float shadowBias,float shadowRadius,vec4 shadowCoord){


float shadow=1.0;
shadowCoord.xyz/=shadowCoord.w;
shadowCoord.z+=shadowBias;


bool inFrustum=shadowCoord.x>=0.0 && shadowCoord.x<=1.0 && shadowCoord.y>=0.0 && shadowCoord.y<=1.0;
bool frustumTest=inFrustum && shadowCoord.z<=1.0;


if(frustumTest){
#if defined(SHADOWMAP_TYPE_VSM)
shadow=VSMShadow(shadowMap,shadowCoord.xy,shadowCoord.z);
#else // no percentage-closer filtering:
shadow=texture2DCompare(shadowMap,shadowCoord.xy,shadowCoord.z);
#endif
}


return mix(1.0,shadow,shadowIntensity);


}


vec2 cubeToUV(vec3 v,float texelSizeY){	
vec3 absV=abs(v);
float scaleToCube=1.0/max(absV.x,max(absV.y,absV.z));
absV*=scaleToCube;
v*=scaleToCube*(1.0-2.0*texelSizeY);
vec2 planar=v.xy;
float almostATexel=1.5*texelSizeY;
float almostOne=1.0-almostATexel;
if(absV.z>=almostOne){
if(v.z>0.0)
planar.x=4.0-v.x;
}else if(absV.x>=almostOne){
float signX=sign(v.x);
planar.x=v.z*signX+2.0*signX;
}else if(absV.y>=almostOne){
float signY=sign(v.y);
planar.x=v.x+2.0*signY+2.0;
planar.y=v.z*signY-2.0;
}
return vec2(0.125,0.25)*planar+vec2(0.375,0.75);
}


float getPointShadow(sampler2D shadowMap,vec2 shadowMapSize,float shadowIntensity,float shadowBias,float shadowRadius,vec4 shadowCoord,float shadowCameraNear,float shadowCameraFar){
float shadow=1.0;
vec3 lightToPosition=shadowCoord.xyz;
float lightToPositionLength=length(lightToPosition);
if(lightToPositionLength-shadowCameraFar<=0.0 && lightToPositionLength-shadowCameraNear>=0.0){
float dp=(lightToPositionLength-shadowCameraNear)/(shadowCameraFar-shadowCameraNear);
dp+=shadowBias;
vec3 bd3D=normalize(lightToPosition);
vec2 texelSize=vec2(1.0)/(shadowMapSize*vec2(4.0,2.0));
shadow=texture2DCompare(shadowMap,cubeToUV(bd3D,texelSize.y),dp);
}
return mix(1.0,shadow,shadowIntensity);
}


// SHADOWMASK_PARS_FRAGMENT


float getShadowMask(){


float shadow=1.0;
DirectionalLightShadow directionalLight;


#pragma unroll_loop_start
for(int i=0;i<NUM_DIR_LIGHT_SHADOWS;i++){
directionalLight=directionalLightShadows[i];
shadow*=getShadow(directionalShadowMap[i],directionalLight.shadowMapSize,directionalLight.shadowIntensity,directionalLight.shadowBias,directionalLight.shadowRadius,vDirectionalShadowCoord[i]);
}
#pragma unroll_loop_end


return shadow;


}

void main(){

float shadow=getShadowMask();
gl_FragColor=vec4(1.0,1.0,1.0,1.0);
gl_FragColor.rgb*=shadow;

2 Likes