@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
173 lines (154 loc) • 7.58 kB
text/typescript
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
mode: ShadowMode = ShadowMode.ShadowMask;
//@type UnityEngine.Color
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;
}
}
}