import { AdditiveBlending, Material, Mesh, MeshBasicMaterial, MeshStandardMaterial,ShadowMaterial } from "three"; import { ObjectUtils, PrimitiveType } from "../engine/engine_create_objects.js"; import { serializable } from "../engine/engine_serialization_decorator.js"; import { RGBAColor } from "../engine/js-extensions/index.js"; import { Behaviour } from "./Component.js"; /** * The mode of the ShadowCatcher. * - ShadowMask: only renders shadows. * - Additive: renders shadows additively. * - Occluder: occludes light. */ enum ShadowMode { ShadowMask = 0, Additive = 1, Occluder = 2, } /** * ShadowCatcher can be added to an Object3D to make it render shadows (or light) in the scene. It can also be used to create a shadow mask, or to occlude light. * If the GameObject is a Mesh, it will apply a shadow-catching material to it - otherwise it will create a quad with the shadow-catching material. * * Note that ShadowCatcher meshes are not raycastable by default; if you want them to be raycastable, change the layers in `onEnable()`. * @category Rendering * @group Components */ export class ShadowCatcher extends Behaviour { //@type Needle.Engine.ShadowCatcher.Mode @serializable() mode: ShadowMode = ShadowMode.ShadowMask; //@type UnityEngine.Color @serializable(RGBAColor) shadowColor: RGBAColor = new RGBAColor(0, 0, 0, 1); private targetMesh?: Mesh; /** @internal */ start() { // if there's no geometry, make a basic quad if (!(this.gameObject instanceof Mesh)) { const quad = ObjectUtils.createPrimitive(PrimitiveType.Quad, { name: "ShadowCatcher", material: new MeshStandardMaterial({ // HACK heuristic to get approx. the same colors out as with the current default ShadowCatcher material // not clear why this is needed; assumption is that the Renderer component does something we're not respecting here color: 0x999999, roughness: 1, metalness: 0, transparent: true, }) }); quad.receiveShadow = true; quad.geometry.rotateX(-Math.PI / 2); // TODO breaks shadow catching right now // const renderer = new Renderer(); // renderer.receiveShadows = true; // GameObject.addComponent(quad, Renderer); this.gameObject.add(quad); this.targetMesh = quad; } else if (this.gameObject instanceof Mesh && this.gameObject.material) { // make sure we have a unique material to work with this.gameObject.material = this.gameObject.material.clone(); this.targetMesh = this.gameObject; // make sure the mesh can receive shadows this.targetMesh.receiveShadow = true; } if(!this.targetMesh) { console.warn("ShadowCatcher: no mesh to apply shadow catching to. Groups are currently not supported."); return; } // Shadowcatcher mesh isnt raycastable this.targetMesh.layers.set(2); switch (this.mode) { case ShadowMode.ShadowMask: this.applyShadowMaterial(); break; case ShadowMode.Additive: this.applyLightBlendMaterial(); break; case ShadowMode.Occluder: this.applyOccluderMaterial(); break; } } // Custom blending, diffuse-only lighting blended onto the scene additively. // Works great for Point Lights and spot lights, // doesn't work for directional lights (since they're lighting up everything else). // Works even better with an additional black-ish gradient to darken parts of the AR scene // so that lights become more visible on bright surfaces. applyLightBlendMaterial() { if (!this.targetMesh) return; const material = this.targetMesh.material as Material; material.blending = AdditiveBlending; this.applyMaterialOptions(material); material.onBeforeCompile = (shader) => { // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib/meshphysical.glsl.js#L181 // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib.js#LL284C11-L284C11 // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib/shadow.glsl.js#L40 // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl.js#L2 // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js#L281 shader.fragmentShader = shader.fragmentShader.replace("vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;", `vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance; // diffuse-only lighting with overdrive to somewhat compensate // for the loss of indirect lighting and to make it more visible. vec3 direct = (reflectedLight.directDiffuse + reflectedLight.directSpecular) * 6.6; float max = max(direct.r, max(direct.g, direct.b)); // early out - we're simply returning direct lighting and some alpha based on it so it can // be blended onto the scene. gl_FragColor = vec4(direct, max); return; `); } material.userData.isLightBlendMaterial = true; } // ShadowMaterial: only does a mask; shadowed areas are fully black. // doesn't take light attenuation into account. // works great for Directional Lights. applyShadowMaterial() { if (this.targetMesh) { if ((this.targetMesh.material as Material).type !== "ShadowMaterial") { const material = new ShadowMaterial(); material.color = this.shadowColor; material.opacity = this.shadowColor.alpha; this.applyMaterialOptions(material); this.targetMesh.material = material; material.userData.isShadowCatcherMaterial = true; } else { const material = this.targetMesh.material as ShadowMaterial; material.color = this.shadowColor; material.opacity = this.shadowColor.alpha; this.applyMaterialOptions(material); material.userData.isShadowCatcherMaterial = true; } } } applyOccluderMaterial() { if (this.targetMesh) { let material = this.targetMesh.material as Material; if (!material) { const mat = new MeshBasicMaterial(); this.targetMesh.material = mat; material = mat; } material.depthWrite = true; material.stencilWrite = true; material.colorWrite = false; this.gameObject.renderOrder = -100; } } private applyMaterialOptions(material: Material) { if (material) { material.depthWrite = false; material.stencilWrite = false; } } }