@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.
342 lines • 15.4 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Object3D, Vector3 } from "three";
import { Gizmos } from "../engine/engine_gizmos.js";
import { MaterialPropertyBlock } from "../engine/engine_materialpropertyblock.js";
import { Mathf } from "../engine/engine_math.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { getTempVector } from "../engine/engine_three_utils.js";
import { getParam } from "../engine/engine_utils.js";
import { Behaviour } from "./Component.js";
import { Renderer } from "./Renderer.js";
const debugSeeThrough = getParam("debugseethrough");
// type MaterialState = {
// opacity: number,
// transparent: boolean,
// alphaHash: boolean
// }
// type MaterialWithState = Material & {
// /** Original values */
// userData: {
// seeThrough: {
// initial: MaterialState,
// }
// }
// };
let i = 0;
/**
* Automatically fades objects to transparent when they obscure a reference point from the camera's view.
* Perfect for architectural visualization, third-person games, or any scenario where objects should
* become see-through when blocking the view of important content.
*
* [](https://engine.needle.tools/samples/see-through)
*
* **How it works:**
* - Monitors the angle between the camera, this object, and a reference point
* - When the object blocks the view to the reference point, it fades out
* - Automatically affects all {@link Renderer} components on this object and children
* - Supports both transparent fading and alpha hash (dithered) fading
*
* **Key Features:**
* - Smooth fade transitions with configurable duration
* - Optional alpha hash for maintaining opaque rendering (better performance)
* - Automatic or manual update modes
* - Disables raycasting when faded (objects become click-through)
* - Preserves original material properties when re-enabled
*
* **Configuration:**
* - `referencePoint` - Object to keep visible (defaults to scene root)
* - `fadeDuration` - Transition speed (default: 0.05 seconds)
* - `minAlpha` - Minimum opacity when faded (default: 0 = fully transparent)
* - `useAlphaHash` - Use dithered transparency instead of true transparency (default: true)
*
* **Performance:**
* - Materials are cloned once per renderer to avoid affecting shared materials
* - Updates direction calculation every 20 frames by default (configurable via `autoUpdate`)
* - Use `needsUpdate = true` to force immediate recalculation
*
* **Requirements:**
* Requires at least one {@link Renderer} component on the same object or child objects.
*
* @example Make walls transparent when blocking view
* ```ts
* // Add to walls or obstacles
* const seeThrough = wall.addComponent(SeeThrough);
* seeThrough.referencePoint = player; // Keep player visible
* seeThrough.fadeDuration = 0.2; // Smooth fade
* seeThrough.minAlpha = 0.2; // Slightly visible when faded
* ```
*
* @example Third-person camera with see-through objects
* ```ts
* const character = GameObject.findByName("Character");
* const obstacles = GameObject.findByTag("Obstacle");
*
* for (const obstacle of obstacles) {
* const st = obstacle.addComponent(SeeThrough);
* st.referencePoint = character;
* st.useAlphaHash = true; // Better performance
* }
* ```
*
* @example Manual control of see-through effect
* ```ts
* const seeThrough = this.gameObject.getComponent(SeeThrough);
* if (seeThrough) {
* seeThrough.autoUpdate = false; // Disable automatic fading
*
* // Manually control transparency
* seeThrough.updateAlpha(0.5, 0.3); // Fade to 50% over 0.3 seconds
*
* // Or use override for precise control
* seeThrough.overrideAlpha = 0.8; // Force 80% opacity
* }
* ```
*
* @summary Fades objects when they obscure the camera's view of a reference point
* @category Rendering
* @group Components
* @see {@link Renderer} for material/rendering control (required)
* @see {@link Camera} for camera setup and configuration
* @see {@link OrbitControls} for camera controls in similar use cases
* @link https://see-through-walls-z23hmxbz1kjfjn.needle.run/ for live demo
* @link https://engine.needle.tools/samples/see-through for sample project
*/
export class SeeThrough extends Behaviour {
/**
* Assign a reference point - if this point will be obscured from the camera by this object then this object will fade out.
* If no reference point is assigned the scene's root object will be used as reference point.
*/
referencePoint = null;
/**
* Fade Duration in seconds
* @default 0.05
*/
fadeDuration = .05;
/**
* Minimum alpha value when fading out (0-1)
* @default 0
*/
minAlpha = 0;
/**
* When useAlphaHash is enabled the object will fade out using alpha hashing, this means the object can stay opaque. If disabled the object will set to be transparent when fading out.
* @default true
*/
useAlphaHash = true;
/**
* Set this to force updating the reference point position and direction
*/
set needsUpdate(val) {
this._needsUpdate = val;
}
get needsUpdate() {
return this._needsUpdate;
}
/**
* Override the alpha value, -1 means no override
* @default -1
*/
overrideAlpha = -1;
/**
*
*/
autoUpdate = true;
_referencePointVector = new Vector3();
_referencePointDir = new Vector3();
_distance = 0;
_renderer = null;
_needsUpdate = true;
_id = i++;
/** * @internal */
onEnable() {
this._needsUpdate = true;
this._renderer = null;
// SeeThroughUsdzExporterPlugin.components.push(this);
}
/** @internal */
onDisable() {
// this._renderer?.forEach(r => {
// const original = this.rendererMaterialsOriginal.get(r);
// for (let i = 0; i < r.sharedMaterials.length; i++) {
// const mat = r.sharedMaterials[i];
// if (!mat) continue;
// if (original && original[i]) {
// r.sharedMaterials[i] = original[i];
// }
// }
// this.rendererMaterials.delete(r);
// this.rendererMaterialsOriginal.delete(r);
// });
// const index = SeeThroughUsdzExporterPlugin.components.indexOf(this);
// if (index !== -1) SeeThroughUsdzExporterPlugin.components.splice(index, 1);
}
/**
* @internal
*/
update() {
if (this._needsUpdate) {
this._needsUpdate = false;
this._renderer = this.gameObject.getComponentsInChildren(Renderer);
// NOTE: instead of using the object's anchor (gameObject.worldPosition) we could also get the object's bounding box center:
// getBoundingBox(this.gameObject); // < import { getBoundingBox } from "@needle-tools/engine";
this.updateDirection();
}
else if (this.autoUpdate && (this.context.time.frame + this._id) % 20 === 0) {
this.updateDirection();
}
if (!this.autoUpdate)
return;
if (!this.referencePoint)
return;
const dot = this._referencePointDir.dot(this.context.mainCamera.worldForward);
const shouldHide = dot > .2;
if (debugSeeThrough && this.referencePoint) {
const wp = this.gameObject.worldPosition;
Gizmos.DrawArrow(getTempVector(wp), wp.sub(this._referencePointDir), shouldHide ? 0xFF0000 : 0x00FF00);
Gizmos.DrawWireSphere(this.referencePoint.worldPosition, .05, 0x0000FF);
}
if (shouldHide) {
this.updateAlpha(this.minAlpha, this.fadeDuration);
}
else {
this.updateAlpha(1, this.fadeDuration);
}
}
// private readonly rendererMaterials = new WeakMap<Renderer, Array<MaterialWithState>>();
// private readonly rendererMaterialsOriginal = new WeakMap<Renderer, Array<Material>>();
updateDirection() {
this.referencePoint ??= this.context.scene;
this._referencePointVector.copy(this.gameObject.worldPosition.sub(this.referencePoint.worldPosition));
this._distance = this._referencePointVector.length();
this._referencePointDir.copy(this._referencePointVector)
.multiply(getTempVector(1, .5, 1)) // Reduce vertical influence
.normalize();
}
/**
* Update the alpha of the object's materials towards the target alpha over the given duration.
* @param targetAlpha Target alpha value (0-1)
* @param duration Duration in seconds to reach the target alpha. 0 means immediate. Default is the component's fadeDuration.
*/
updateAlpha(targetAlpha, duration = this.fadeDuration) {
if (this.overrideAlpha !== undefined && this.overrideAlpha !== -1) {
targetAlpha = this.overrideAlpha;
}
this._renderer?.forEach(renderer => {
if (targetAlpha < .9) {
renderer.gameObject.raycastAllowed = false;
}
else {
renderer.gameObject.raycastAllowed = true;
}
// if (!this.rendererMaterials.has(renderer)) {
// const originalMaterials = new Array<Material>();
// const clonedMaterials = new Array<MaterialWithState>();
// // We clone the materials once and store them, so we can modify the opacity without affecting other objects using the same material. This could potentially be optimized further to re-use materials between renderers if multiple renderers use the same material.
// for (let i = 0; i < renderer.sharedMaterials.length; i++) {
// const mat = renderer.sharedMaterials[i];
// if (!mat) continue;
// originalMaterials.push(mat);
// const matClone = mat.clone() as MaterialWithState;
// // @ts-ignore
// matClone.userData = mat.userData || {};
// matClone.userData.seeThrough = {
// initial: {
// opacity: matClone.opacity,
// transparent: matClone.transparent,
// alphaHash: matClone.alphaHash
// }
// }
// clonedMaterials.push(matClone);
// // renderer.sharedMaterials[i] = matClone;
// }
// this.rendererMaterials.set(renderer, clonedMaterials);
// this.rendererMaterialsOriginal.set(renderer, originalMaterials);
// }
const materials = renderer.sharedMaterials; // : this.rendererMaterials.get(renderer);
if (!materials)
return;
const block = MaterialPropertyBlock.get(renderer.gameObject);
const currentOpacity = (block.getOverride("opacity")?.value ?? materials[0].opacity ?? 1);
let newAlpha = Mathf.lerp(currentOpacity, targetAlpha, duration <= 0 ? 1 : this.context.time.deltaTime / duration);
;
if (newAlpha >= 0.99)
newAlpha = 1;
else if (newAlpha <= 0.01)
newAlpha = 0;
// const currentTransparent = (block.getOverride("transparent")?.value ?? materials[0].transparent ?? false) as boolean;
block.setOverride("alphaHash", this.useAlphaHash);
block.setOverride("opacity", newAlpha);
block.setOverride("transparent", newAlpha >= 0.99999 ? false : !this.useAlphaHash);
// for (const mat of materials) {
// if (!mat) continue;
// let newAlpha = Mathf.lerp(mat.opacity, targetAlpha, duration <= 0 ? 1 : this.context.time.deltaTime / duration);;
// if (newAlpha >= 0.99) newAlpha = 1;
// else if (newAlpha <= 0.01) newAlpha = 0;
// const wasTransparent = mat.transparent;
// const wasAlphaHash = mat.alphaHash;
// const previousOpacity = mat.opacity;
// mat.alphaHash = this.useAlphaHash;
// if (mat.userData && "seeThrough" in mat.userData) {
// const initial = mat.userData.seeThrough.initial as MaterialState;
// mat.opacity = initial.opacity * newAlpha;
// mat.transparent = mat.opacity >= 1 ? initial.transparent : !this.useAlphaHash;
// }
// else {
// mat.transparent = mat.opacity >= 1 ? false : !this.useAlphaHash;
// }
// if (wasTransparent !== mat.transparent
// || wasAlphaHash !== mat.alphaHash
// || mat.opacity !== previousOpacity // MeshPhysicsMaterial needs that and maybe other materials too...
// ) {
// mat.needsUpdate = true;
// }
// }
});
}
}
__decorate([
serializable(Object3D)
], SeeThrough.prototype, "referencePoint", void 0);
__decorate([
serializable()
], SeeThrough.prototype, "fadeDuration", void 0);
__decorate([
serializable()
], SeeThrough.prototype, "minAlpha", void 0);
__decorate([
serializable()
], SeeThrough.prototype, "useAlphaHash", void 0);
__decorate([
serializable()
], SeeThrough.prototype, "overrideAlpha", void 0);
__decorate([
serializable()
], SeeThrough.prototype, "autoUpdate", void 0);
;
// class SeeThroughUsdzExporterPlugin implements IUSDExporterExtension {
// static readonly components: SeeThrough[] = [];
// get extensionName() {
// return "SeeThrough";
// }
// // onExportObject(object: Object3D<Object3DEventMap>, model: USDObject, context: USDZExporterContext) {
// // const component = SeeThroughUsdzExporterPlugin.components.find(c => c.gameObject === object);
// // if(!component) return;
// // console.log("OH MY GOD SEE THROUGH USDZ EXPORTER", component, model);
// // model.materialName = "AlphaHashMaterialInstance"; // we could make this unique per object if needed
// // model.addEventListener("serialize", (writer, context) => {
// // writer.appendLine(`# SeeThrough component on ${object.name}`);
// // });
// // }
// }
// const seeThroughUsdzExporterPlugin = new SeeThroughUsdzExporterPlugin();
// USDZExporter.beforeExport.addEventListener(args => {
// if (SeeThroughUsdzExporterPlugin.components.length === 0) return;
// if (args.exporter.extensions.includes(seeThroughUsdzExporterPlugin) === false) {
// args.exporter.extensions.push(seeThroughUsdzExporterPlugin);
// }
// });
//# sourceMappingURL=SeeThrough.js.map