jnsmalm / pixi3d

The 3D renderer for PixiJS. Seamless integration with 2D applications.
https://pixi3d.org
MIT License
752 stars 43 forks source link

Render shadow on "invisible" plane. #175

Closed gfcrba closed 1 year ago

gfcrba commented 1 year ago

Hey! Thanks for great library it works great!

I've got a question and haven't found answer in docs, but sure it can be helpful in some situations for everyone. I need to have fully transparent plane on stage the only purpose of which is to render shadows on it.

Can I do it somehow with tools we have right now or maybe you can point on approach I should use to do it?

jnsmalm commented 1 year ago

Hello and thanks!

For now you need to create a custom material to support this. Maybe I can add this feature in StandardMaterial at a later time.

This is the code for the material (please note that this code will only work when using WebGL 2, let me know if you need to support WebGL 1 as well.):

let vert = `#version 300 es

  precision highp float;

  in vec3 a_Position;

  uniform mat4 u_Model;
  uniform mat4 u_ViewProjection;
  uniform mat4 u_LightViewProjectionMatrix;

  out vec4 v_PositionLightSpace;

  void main() {
    vec4 pos = u_Model * vec4(a_Position, 1.0);
    v_PositionLightSpace = u_LightViewProjectionMatrix * pos;
    gl_Position = u_ViewProjection * u_Model * vec4(a_Position, 1.0);
  }`

let frag = `#version 300 es

  precision highp float;

  in vec4 v_PositionLightSpace;

  out vec4 fragColor;

  uniform vec3 u_Color;
  uniform sampler2D u_ShadowSampler;
  uniform vec4 u_ShadowColor;

  float linstep(float low, float high, float v) {
    return clamp((v-low) / (high-low), 0.0, 1.0);
  }

  float getShadowContribution() {
    vec3 coords = v_PositionLightSpace.xyz / v_PositionLightSpace.w * 0.5 + 0.5;
    if (coords.z < 0.01 || coords.z > 0.99 || coords.x < 0.01 || coords.x > 0.99 || coords.y < 0.01 || coords.y > 0.99) {
        return 1.0;
    }
    vec2 moments = vec2(1.0) - texture(u_ShadowSampler, coords.xy).xy;
    float p = step(coords.z, moments.x);
    float variance = max(moments.y - moments.x * moments.x, 0.00002);
    float d = coords.z - moments.x;
    float pMax = linstep(0.2, 1.0, variance / (variance + d*d));
    return min(max(p, pMax), 1.0);
  }

  void main() {
    float shadow = 1.0 - getShadowContribution();
    vec4 shadowColor = u_ShadowColor * vec4(shadow);
    float shadowAlpha = shadow * shadowColor.a;
    fragColor = vec4(shadowColor.rgb * shadowAlpha, shadowAlpha);
  }`

class TransparentShadowReceiverMaterial extends PIXI3D.Material {
  constructor(shadowCastingLight) {
    super()
    this._shadowCastingLight = shadowCastingLight
    this.shadowColor = new PIXI3D.Color(0, 0, 0, 0.25)
  }

  updateUniforms(mesh, shader) {
    shader.uniforms.u_Model = mesh.worldTransform.array
    shader.uniforms.u_ViewProjection = PIXI3D.Camera.main.viewProjection.array
    shader.uniforms.u_Color = [255, 0, 255]
    if (this._shadowCastingLight) {
      shader.uniforms.u_ShadowSampler = this._shadowCastingLight.shadowTexture
      shader.uniforms.u_LightViewProjectionMatrix = this._shadowCastingLight.lightViewProjection
      shader.uniforms.u_ShadowColor = this.shadowColor.rgba
    }
  }

  createShader() {
    return new PIXI3D.MeshShader(PIXI.Program.from(vert, frag))
  }
}

Here is a complete example:

let app = new PIXI.Application({
  backgroundColor: 0xff00ff, resizeTo: window, antialias: true
})
document.body.appendChild(app.view)

let control = new PIXI3D.CameraOrbitControl(app.view)
control.enableDamping = true

app.loader.add("assets/chromatic/diffuse.cubemap")
app.loader.add("assets/chromatic/specular.cubemap")
app.loader.add("assets/teapot/teapot.gltf")

let vert = `#version 300 es

  precision highp float;

  in vec3 a_Position;

  uniform mat4 u_Model;
  uniform mat4 u_ViewProjection;
  uniform mat4 u_LightViewProjectionMatrix;

  out vec4 v_PositionLightSpace;

  void main() {
    vec4 pos = u_Model * vec4(a_Position, 1.0);
    v_PositionLightSpace = u_LightViewProjectionMatrix * pos;
    gl_Position = u_ViewProjection * u_Model * vec4(a_Position, 1.0);
  }`

let frag = `#version 300 es

  precision highp float;

  in vec4 v_PositionLightSpace;

  out vec4 fragColor;

  uniform vec3 u_Color;
  uniform sampler2D u_ShadowSampler;
  uniform vec4 u_ShadowColor;

  float linstep(float low, float high, float v) {
    return clamp((v-low) / (high-low), 0.0, 1.0);
  }

  float getShadowContribution() {
    vec3 coords = v_PositionLightSpace.xyz / v_PositionLightSpace.w * 0.5 + 0.5;
    if (coords.z < 0.01 || coords.z > 0.99 || coords.x < 0.01 || coords.x > 0.99 || coords.y < 0.01 || coords.y > 0.99) {
        return 1.0;
    }
    vec2 moments = vec2(1.0) - texture(u_ShadowSampler, coords.xy).xy;
    float p = step(coords.z, moments.x);
    float variance = max(moments.y - moments.x * moments.x, 0.00002);
    float d = coords.z - moments.x;
    float pMax = linstep(0.2, 1.0, variance / (variance + d*d));
    return min(max(p, pMax), 1.0);
  }

  void main() {
    float shadow = 1.0 - getShadowContribution();
    vec4 shadowColor = u_ShadowColor * vec4(shadow);
    float shadowAlpha = shadow * shadowColor.a;
    fragColor = vec4(shadowColor.rgb * shadowAlpha, shadowAlpha);
  }`

class TransparentShadowReceiverMaterial extends PIXI3D.Material {
  constructor(shadowCastingLight) {
    super()
    this._shadowCastingLight = shadowCastingLight
    this.shadowColor = new PIXI3D.Color(0, 0, 0, 0.25)
  }

  updateUniforms(mesh, shader) {
    shader.uniforms.u_Model = mesh.worldTransform.array
    shader.uniforms.u_ViewProjection = PIXI3D.Camera.main.viewProjection.array
    shader.uniforms.u_Color = [255, 0, 255]
    if (this._shadowCastingLight) {
      shader.uniforms.u_ShadowSampler = this._shadowCastingLight.shadowTexture
      shader.uniforms.u_LightViewProjectionMatrix = this._shadowCastingLight.lightViewProjection
      shader.uniforms.u_ShadowColor = this.shadowColor.rgba
    }
  }

  createShader() {
    return new PIXI3D.MeshShader(PIXI.Program.from(vert, frag))
  }
}

app.loader.load((_, resources) => {
  PIXI3D.LightingEnvironment.main.imageBasedLighting = new PIXI3D.ImageBasedLighting(
    resources["assets/chromatic/diffuse.cubemap"].cubemap,
    resources["assets/chromatic/specular.cubemap"].cubemap
  )

  let model = app.stage.addChild(
    PIXI3D.Model.from(resources["assets/teapot/teapot.gltf"].gltf))
  model.y = -0.8
  model.meshes.forEach(mesh => {
    mesh.material.exposure = 1.3
  })

  let ground = app.stage.addChild(PIXI3D.Mesh3D.createPlane())
  ground.y = -0.8
  ground.scale.set(10, 1, 10)

  let directionalLight = Object.assign(new PIXI3D.Light(), {
    intensity: 1,
    type: "directional"
  })
  directionalLight.rotationQuaternion.setEulerAngles(25, 120, 0)
  PIXI3D.LightingEnvironment.main.lights.push(directionalLight)

  let shadowCastingLight = new PIXI3D.ShadowCastingLight(
    app.renderer, directionalLight, { shadowTextureSize: 1024, quality: PIXI3D.ShadowQuality.medium })
  shadowCastingLight.softness = 1
  shadowCastingLight.shadowArea = 15

  ground.material = new TransparentShadowReceiverMaterial(shadowCastingLight)

  let pipeline = app.renderer.plugins.pipeline
  pipeline.enableShadows(ground, shadowCastingLight)
  pipeline.enableShadows(model, shadowCastingLight)
})
jnsmalm commented 1 year ago

Should look like this

image
gfcrba commented 1 year ago

Thank you very much. It works like a charm! You saved me a ton of time. Do you have a patreon or smth for small thank you?

jnsmalm commented 1 year ago

No problem!

gfcrba commented 1 year ago

Oh just one notice for ios (at least for 16.4). uniform vec3 u_Color; Not used in fragment shader, so it optimise it somehow and reduces uniform buffer so data passing become corrupted. Hope this will help someone as well.