@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.
904 lines (785 loc) • 35.6 kB
text/typescript
import { getRaycastMesh } from "@needle-tools/gltf-progressive";
import { AxesHelper, Material, Mesh, Object3D, SkinnedMesh, Texture, Vector4 } from "three";
import { showBalloonWarning } from "../engine/debug/index.js";
import { getComponent, getOrAddComponent } from "../engine/engine_components.js";
import { Gizmos } from "../engine/engine_gizmos.js";
import { InstancingUtil, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js";
import { isLocalNetwork } from "../engine/engine_networking_utils.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { FrameEvent } from "../engine/engine_setup.js";
import { getTempVector } from "../engine/engine_three_utils.js";
import type { IRenderer, ISharedMaterials } from "../engine/engine_types.js";
import { getParam } from "../engine/engine_utils.js";
import { NEEDLE_render_objects } from "../engine/extensions/NEEDLE_render_objects.js";
import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
import { Behaviour, GameObject } from "./Component.js";
import { ReflectionProbe } from "./ReflectionProbe.js";
import { InstanceHandle, InstancingHandler } from "./RendererInstancing.js"
// import { RendererCustomShader } from "./RendererCustomShader.js";
import { RendererLightmap } from "./RendererLightmap.js";
// for staying compatible with old code
export { InstancingUtil } from "../engine/engine_instancing.js";
const debugRenderer = getParam("debugrenderer");
const debugskinnedmesh = getParam("debugskinnedmesh");
const suppressInstancing = getParam("noinstancing");
const showWireframe = getParam("wireframe");
export enum ReflectionProbeUsage {
Off = 0,
BlendProbes = 1,
BlendProbesAndSkybox = 2,
Simple = 3,
}
export class FieldWithDefault {
public path: string | null = null;
public asset: object | null = null;
public default: any;
}
export enum RenderState {
Both = 0,
Back = 1,
Front = 2,
}
// support sharedMaterials[index] assigning materials directly to the objects
class SharedMaterialArray implements ISharedMaterials {
[num: number]: Material;
private _renderer: Renderer;
private _targets: Object3D[] = [];
private _indexMapMaxIndex?: number;
private _indexMap?: Map<number, number>;
private _changed: boolean = false;
get changed(): boolean {
return this._changed;
}
set changed(value: boolean) {
if (value === true) {
if (debugRenderer)
console.warn("SharedMaterials have changed: " + this._renderer.name, this);
}
this._changed = value;
}
is(renderer: Renderer) {
return this._renderer === renderer;
}
constructor(renderer: Renderer, originalMaterials: Material[]) {
this._renderer = renderer;
const setMaterial = this.setMaterial.bind(this);
const getMaterial = this.getMaterial.bind(this);
const go = renderer.gameObject;
this._targets = [];
if (go) {
switch (go.type) {
case "Group":
this._targets = [...go.children];
break;
case "SkinnedMesh":
case "Mesh":
this._targets.push(go);
break;
}
}
// this is useful to have an index map when e.g. materials are trying to be assigned by index
let hasMissingMaterials = false;
let indexMap: Map<number, number> | undefined = undefined;
let maxIndex: number = 0;
for (let i = 0; i < this._targets.length; i++) {
const target = this._targets[i] as Mesh;
if (!target) continue;
const mat = target.material as Material;
if (!mat) continue;
// set the shadow side to the same as the side of the material, three flips this for some reason
mat.shadowSide = mat.side;
for (let k = 0; k < originalMaterials.length; k++) {
const orig = originalMaterials[k];
if (!orig) {
hasMissingMaterials = true;
continue;
}
if (mat.name === orig.name) {
if (indexMap === undefined) indexMap = new Map();
indexMap.set(k, i);
maxIndex = Math.max(maxIndex, k);
// console.log(`Material ${mat.name} at ${k} was found at index ${i} in renderer ${renderer.name}.`)
break;
}
}
}
if (hasMissingMaterials) {
this._indexMapMaxIndex = maxIndex;
this._indexMap = indexMap;
const warningMessage = `Renderer ${renderer.name} was initialized with missing materials - this may lead to unexpected behaviour when trying to access sharedMaterials by index.`;
console.warn(warningMessage);
if (isLocalNetwork()) showBalloonWarning("Found renderer with missing materials: please check the console for details.");
}
// this lets us override the javascript indexer, only works in ES6 tho
// but like that we can use sharedMaterials[index] and it will be assigned to the object directly
return new Proxy(this, {
get(target, key) {
if (typeof key === "string") {
const index = parseInt(key);
if (!isNaN(index)) {
return getMaterial(index);
}
}
return target[key];
},
set(target, key, value) {
if (typeof key === "string")
setMaterial(value, Number.parseInt(key));
// console.log(target, key, value);
if (Reflect.set(target, key, value)) {
if (value instanceof Material)
target.changed = true;
return true;
}
return false;
}
});
}
get length(): number {
if (this._indexMapMaxIndex !== undefined) return this._indexMapMaxIndex + 1;
return this._targets.length;
}
// iterator to support: for(const mat of sharedMaterials)
*[Symbol.iterator]() {
for (let i = 0; i < this.length; i++) {
yield this.getMaterial(i);
}
}
private resolveIndex(index: number): number {
const map = this._indexMap;
// if we have a index map it means that some materials were missing
if (map) {
if (map.has(index)) return map.get(index) as number;
// return -1;
}
return index;
}
private setMaterial(mat: Material, index: number) {
index = this.resolveIndex(index);
if (index < 0 || index >= this._targets.length) return;
const target = this._targets[index];
if (!target || target["material"] === undefined) return;
target["material"] = mat;
this.changed = true;
}
private getMaterial(index: number): Material | null {
index = this.resolveIndex(index);
if (index < 0) return null;
const obj = this._targets;
if (index >= obj.length) return null;
const target = obj[index];
if (!target) return null;
return target["material"];
}
}
/**
* @category Rendering
* @group Components
*/
export class Renderer extends Behaviour implements IRenderer {
/** Enable or disable instancing for an object. This will create a Renderer component if it does not exist yet.
* @returns the Renderer component that was created or already existed on the object
*/
static setInstanced(obj: Object3D, enableInstancing: boolean): Renderer {
const renderer = getOrAddComponent(obj, Renderer);
renderer.setInstancingEnabled(enableInstancing);
return renderer;
}
/** Check if an object is currently rendered using instancing
* @returns true if the object is rendered using instancing
*/
static isInstanced(obj: Object3D): boolean {
const renderer = getComponent(obj, Renderer);
if (renderer) return renderer.isInstancingActive;
return InstancingUtil.isUsingInstancing(obj);
}
/** Set the rendering state only of an object (makes it visible or invisible) without affecting component state or child hierarchy visibility! You can also just enable/disable the Renderer component on that object for the same effect!
*
* If you want to activate or deactivate a complete object you can use obj.visible as usual (it acts the same as setActive in Unity) */
static setVisible(obj: Object3D, visible: boolean) {
setCustomVisibility(obj, visible);
}
receiveShadows: boolean = false;
shadowCastingMode: ShadowCastingMode = ShadowCastingMode.Off;
lightmapIndex: number = -1;
lightmapScaleOffset: Vector4 = new Vector4(1, 1, 0, 0);
/** If the renderer should use instancing
* If this is a boolean (true) all materials will be instanced or (false) none of them.
* If this is an array of booleans the materials will be instanced based on the index of the material.
*/
enableInstancing: boolean | boolean[] | undefined = undefined;
renderOrder: number[] | undefined = undefined;
allowOcclusionWhenDynamic: boolean = true;
probeAnchor?: Object3D;
reflectionProbeUsage: ReflectionProbeUsage = ReflectionProbeUsage.Off;
// custom shader
// get materialProperties(): Array<MaterialProperties> | undefined {
// return this._materialProperties;
// }
// set materialProperties(value: Array<MaterialProperties> | undefined) {
// this._materialProperties = value;
// }
// private customShaderHandler: RendererCustomShader | undefined = undefined;
// private _materialProperties: Array<MaterialProperties> | undefined = undefined;
private _lightmaps?: RendererLightmap[];
/** Get the mesh Object3D for this renderer
* Warn: if this is a multimaterial object it will return the first mesh only
* @returns a mesh object3D.
* */
get sharedMesh(): Mesh | SkinnedMesh | undefined {
if (this.gameObject.type === "Mesh") {
return this.gameObject as unknown as Mesh
}
else if (this.gameObject.type === "SkinnesMesh") {
return this.gameObject as unknown as SkinnedMesh;
}
else if (this.gameObject.type === "Group") {
return this.gameObject.children[0] as unknown as Mesh;
}
return undefined;
}
private readonly _sharedMeshes: Mesh[] = [];
/** Get all the mesh Object3D for this renderer
* @returns an array of mesh object3D.
*/
get sharedMeshes(): Mesh[] {
if (this.destroyed || !this.gameObject) return this._sharedMeshes;
this._sharedMeshes.length = 0;
if (this.gameObject.type === "Group") {
for (const ch of this.gameObject.children) {
if (ch.type === "Mesh" || ch.type === "SkinnedMesh") {
this._sharedMeshes.push(ch as Mesh);
}
}
}
else if (this.gameObject.type === "Mesh" || this.gameObject.type === "SkinnedMesh") {
this._sharedMeshes.push(this.gameObject as unknown as Mesh);
}
return this._sharedMeshes;
}
get sharedMaterial(): Material {
return this.sharedMaterials[0];
}
set sharedMaterial(mat: Material) {
const cur = this.sharedMaterials[0];
if (cur === mat) return;
this.sharedMaterials[0] = mat;
this.applyLightmapping();
}
/**@deprecated please use sharedMaterial */
get material(): Material {
return this.sharedMaterials[0];
}
/**@deprecated please use sharedMaterial */
set material(mat: Material) {
this.sharedMaterial = mat;
}
private _sharedMaterials!: SharedMaterialArray;
private _originalMaterials?: Material[];
private _probeAnchorLastFrame?: Object3D;
// this is just available during deserialization
private set sharedMaterials(_val: Array<Material | null>) {
// TODO: elements in the array might be missing at the moment which leads to problems if an index is serialized
if (!this._originalMaterials) {
this._originalMaterials = _val as Material[];
}
else if (_val) {
let didWarn = false;
for (let i = 0; i < this._sharedMaterials.length; i++) {
const mat = i < _val.length ? _val[i] : null;
if (mat && mat instanceof Material) {
this.sharedMaterials[i] = mat as Material;
}
else {
if (!didWarn) {
didWarn = true;
console.warn("Can not assign null as material: " + this.name, mat);
}
}
}
}
}
//@ts-ignore
get sharedMaterials(): SharedMaterialArray {
if (!this._sharedMaterials || !this._sharedMaterials.is(this)) {
if (!this._originalMaterials) this._originalMaterials = [];
this._sharedMaterials = new SharedMaterialArray(this, this._originalMaterials);
}
return this._sharedMaterials!;
}
public static get shouldSuppressInstancing() {
return suppressInstancing;
}
private _lightmapTextureOverride: Texture | null | undefined = undefined;
public get lightmap(): Texture | null {
if (this._lightmaps?.length) {
return this._lightmaps[0].lightmap;
}
return null;
}
/** set undefined to return to default lightmap */
public set lightmap(tex: Texture | null | undefined) {
this._lightmapTextureOverride = tex;
if (tex === undefined) {
tex = this.context.lightmaps.tryGetLightmap(this.sourceId, this.lightmapIndex);
}
if (this._lightmaps?.length) {
for (const lm of this._lightmaps) {
lm.lightmap = tex;
}
}
}
get hasLightmap(): boolean {
const lm = this.lightmap;
return lm !== null && lm !== undefined;
}
public allowProgressiveLoading: boolean = true;
private _firstFrame: number = -1;
registering() {
if (!this.enabled) {
this.setVisibility(false);
}
}
awake() {
this._firstFrame = this.context.time.frame;
if (debugRenderer) console.log("Renderer ", this.name, this);
this.clearInstancingState();
if (this.probeAnchor && debugRenderer) this.probeAnchor.add(new AxesHelper(.2));
this._reflectionProbe = null;
if (this.isMultiMaterialObject(this.gameObject)) {
for (const child of this.gameObject.children) {
this.context.addBeforeRenderListener(child, this.onBeforeRenderThree);
child.layers.mask = this.gameObject.layers.mask;
}
if (this.renderOrder !== undefined) {
// Objects can have nested renderers (e.g. contain 2 meshes and then again another group)
// or perhaps just regular child objects that have their own renderer component (?)
let index = 0;
for (let i = 0; i < this.gameObject.children.length; i++) {
const ch = this.gameObject.children[i];
// ignore nested groups or objects that have their own renderer (aka their own render order settings)
if (!this.isMeshOrSkinnedMesh(ch) || GameObject.getComponent(ch, Renderer)) continue;
if (this.renderOrder.length <= index) {
console.warn("Incorrect renderOrder element count", this, this.renderOrder.length + " but expected " + this.gameObject.children.length, "Index: " + index, "ChildElement:", ch);
continue;
}
// if(debugRenderer) console.log("Setting render order", ch, this.renderOrder[index])
ch.renderOrder = this.renderOrder[index];
index += 1;
}
}
}
// TODO: custom shader with sub materials
else if (this.isMeshOrSkinnedMesh(this.gameObject)) {
this.context.addBeforeRenderListener(this.gameObject, this.onBeforeRenderThree);
if (this.renderOrder !== undefined && this.renderOrder.length > 0)
this.gameObject.renderOrder = this.renderOrder[0];
}
else {
this.context.addBeforeRenderListener(this.gameObject, this.onBeforeRenderThree);
}
this.applyLightmapping();
if (showWireframe) {
for (let i = 0; i < this.sharedMaterials.length; i++) {
const mat: any = this.sharedMaterials[i];
if (mat) {
mat.wireframe = true;
}
}
}
}
private applyLightmapping() {
if (this.lightmapIndex >= 0) {
const type = this.gameObject.type;
// use the override lightmap if its not undefined
const tex = this._lightmapTextureOverride !== undefined
? this._lightmapTextureOverride
: this.context.lightmaps.tryGetLightmap(this.sourceId, this.lightmapIndex);
if (tex) {
if (!this._lightmaps)
this._lightmaps = [];
if (type === "Mesh") {
const mat = this.gameObject["material"];
if (!mat?.isMeshBasicMaterial) {
if (this._lightmaps.length <= 0) {
const rm = new RendererLightmap(this.gameObject as any as Mesh, this.context);
this._lightmaps.push(rm);
}
const rm = this._lightmaps[0];
rm.init(this.lightmapIndex, this.lightmapScaleOffset, tex);
}
else {
if (mat)
console.warn("Lightmapping is not supported on MeshBasicMaterial", mat.name)
}
}
// for multi materials we need to loop through children
// and then we add a lightmap renderer component to each of them
else if (this.isMultiMaterialObject(this.gameObject) && this.sharedMaterials.length > 0) {
for (let i = 0; i < this.gameObject.children.length; i++) {
const child = this.gameObject.children[i];
if (!child["material"]?.isMeshBasicMaterial) {
let rm: RendererLightmap | undefined = undefined;
if (i >= this._lightmaps.length) {
rm = new RendererLightmap(child as Mesh, this.context);
this._lightmaps.push(rm);
}
else
rm = this._lightmaps[i];
rm.init(this.lightmapIndex, this.lightmapScaleOffset, tex);
}
}
}
}
else {
if (debugRenderer) console.warn("Lightmap not found", this.sourceId, this.lightmapIndex);
}
}
}
private _isInstancingEnabled: boolean = false;
private _handles: InstanceHandle[] | null | undefined = undefined;
/**
* @returns true if this renderer has instanced objects
*/
get isInstancingActive() {
return this._handles != undefined && this._handles.length > 0 && this._isInstancingEnabled;
}
/** @returns the instancing handles */
get instances(): InstanceHandle[] | null {
if (!this._handles || this._handles.length <= 0) {
return null;
}
this._handlesTempArray.length = 0;
if (this._handles) {
for (const h of this._handles) {
this._handlesTempArray.push(h);
}
}
return this._handlesTempArray;
}
private _handlesTempArray: InstanceHandle[] = [];
/** Enable or disable instancing for this renderer.
* @param enabled true to enable instancing, false to disable it
*/
setInstancingEnabled(enabled: boolean): boolean {
if (this._isInstancingEnabled === enabled) return enabled && (this._handles === undefined || this._handles != null && this._handles.length > 0);
this._isInstancingEnabled = enabled;
if (enabled) {
if (this.enableInstancing === undefined) this.enableInstancing = true;
if (this._handles === undefined) {
this._handles = InstancingHandler.instance.setup(this, this.gameObject, this.context, null, { rend: this, foundMeshes: 0, useMatrixWorldAutoUpdate: this.useInstanceMatrixWorldAutoUpdate() });
if (this._handles) {
GameObject.markAsInstancedRendered(this.gameObject, true);
return true;
}
}
else if (this._handles !== null) {
for (const handler of this._handles) {
handler.updateInstanceMatrix(true);
handler.add();
}
GameObject.markAsInstancedRendered(this.gameObject, true);
return true;
}
}
else {
if (this._handles) {
for (const handler of this._handles) {
handler.remove(this.destroyed);
}
}
return true;
}
return false;
}
private clearInstancingState() {
this._isInstancingEnabled = false;
this._handles = undefined;
}
/** Return true to wrap matrix update events for instanced rendering to update instance matrices automatically when matrixWorld changes
* This is a separate method to be overrideable from user code
*/
useInstanceMatrixWorldAutoUpdate() {
return true;
}
start() {
if (this.enableInstancing && !suppressInstancing) {
this.setInstancingEnabled(true);
// make sure the instance is marked dirty once for cases where e.g. an animator animates the instanced object
// in the first frame we want the updated matrix then to be applied immediately to the instancing
InstancingUtil.markDirty(this.gameObject);
}
this.gameObject.frustumCulled = this.allowOcclusionWhenDynamic;
if (this.isMultiMaterialObject(this.gameObject)) {
for (let i = 0; i < this.gameObject.children.length; i++) {
const ch = this.gameObject.children[i];
ch.frustumCulled = this.allowOcclusionWhenDynamic;
}
}
}
onEnable() {
// ensure shared meshes are initialized
const _ = this.sharedMeshes;
this.setVisibility(true);
// Check if the renderer is using instancing (or any child object is supposed to use instancing)
const isUsingInstancing = this._isInstancingEnabled ||
(this.enableInstancing == true || (Array.isArray(this.enableInstancing) && this.enableInstancing.some(x => x)));
if (isUsingInstancing) {
if (this.__internalDidAwakeAndStart) this.setInstancingEnabled(true);
}
// if no insancing is used we can apply the stencil settings
// but instancing and stencil at the same time is not supported
else if (this.enabled) {
this.applyStencil();
}
this.updateReflectionProbe();
// this.testIfLODLevelsAreAvailable();
}
onDisable() {
this.setVisibility(false);
if (this._handles && this._handles.length > 0) {
this.setInstancingEnabled(false);
}
}
onDestroy(): void {
this._handles = null;
if (this.isMultiMaterialObject(this.gameObject)) {
for (const child of this.gameObject.children) {
this.context.removeBeforeRenderListener(child, this.onBeforeRenderThree);
}
}
else {
this.context.removeBeforeRenderListener(this.gameObject, this.onBeforeRenderThree);
}
}
onBeforeRender() {
if (!this.gameObject) {
return;
}
if (this._probeAnchorLastFrame !== this.probeAnchor) {
this._reflectionProbe?.onUnset(this);
this.updateReflectionProbe();
}
if (debugRenderer == this.name && this.gameObject instanceof Mesh) {
this.gameObject.geometry.computeBoundingSphere();
const tempCenter = getTempVector(this.gameObject.geometry.boundingSphere.center).applyMatrix4(this.gameObject.matrixWorld);
Gizmos.DrawWireSphere(tempCenter, this.gameObject.geometry.boundingSphere.radius, 0x00ddff);
}
if (this.isMultiMaterialObject(this.gameObject) && this.gameObject.children?.length > 0) {
for (const ch of this.gameObject.children) {
this.applySettings(ch);
}
}
else {
this.applySettings(this.gameObject);
}
if (this.sharedMaterials.changed) {
this.sharedMaterials.changed = false;
this.applyLightmapping();
}
if (this._handles?.length) {
// if (this.name === "Darbouka")
// console.log(this.name, this.gameObject.matrixWorldNeedsUpdate);
const needsUpdate: boolean = this.gameObject[NEED_UPDATE_INSTANCE_KEY] === true;// || this.gameObject.matrixWorldNeedsUpdate;
if (needsUpdate) {
// if (debugInstancing) console.log("UPDATE INSTANCED MATRICES at frame #" + this.context.time.frame);
this.gameObject[NEED_UPDATE_INSTANCE_KEY] = false;
const remove = false;// Math.random() < .01;
for (let i = this._handles.length - 1; i >= 0; i--) {
const h = this._handles[i];
if (remove) {
h.remove(this.destroyed);
this._handles.splice(i, 1);
}
else
h.updateInstanceMatrix();
}
this.gameObject.matrixWorldNeedsUpdate = false;
}
}
if (this._handles && this._handles.length <= 0) {
GameObject.markAsInstancedRendered(this.gameObject, false);
}
if (this._isInstancingEnabled && this._handles) {
for (let i = 0; i < this._handles.length; i++) {
const handle = this._handles[i];
setCustomVisibility(handle.object, false);
}
}
if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off && this._reflectionProbe) {
this._reflectionProbe.onSet(this);
}
// since three 163 we need to set the envMap to the scene envMap if it is not set
// otherwise the envmapIntensity has no effect: https://github.com/mrdoob/three.js/pull/27903
// internal issue: https://linear.app/needle/issue/NE-6363
for (const mat of this._sharedMaterials) {
// If the material has a envMap and is NOT using a reflection probe we set the envMap to the scene environment
if (mat && "envMap" in mat && "envMapIntensity" in mat && !ReflectionProbe.isUsingReflectionProbe(mat)) {
mat.envMap = this.context.scene.environment;
}
}
}
private onBeforeRenderThree = (_renderer, _scene, _camera, _geometry, material, _group) => {
if (material.envMapIntensity !== undefined) {
const factor = this.hasLightmap ? Math.PI : 1;
const environmentIntensity = this.context.mainCameraComponent?.environmentIntensity ?? 1;
material.envMapIntensity = Math.max(0, environmentIntensity * this.context.sceneLighting.environmentIntensity / factor);
}
if (this._lightmaps) {
for (const lm of this._lightmaps) {
lm.updateLightmapUniforms(material);
lm.applyLightmap();
}
}
}
onAfterRender() {
if (this._isInstancingEnabled && this._handles) {
for (let i = 0; i < this._handles.length; i++) {
const handle = this._handles[i];
setCustomVisibility(handle.object, true);
}
}
if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off && this._reflectionProbe) {
this._reflectionProbe.onUnset(this);
}
if (this.static && this.gameObject.matrixAutoUpdate) {
this.gameObject.matrixAutoUpdate = false;
}
}
/** Applies stencil settings for this renderer's objects (if stencil settings are available) */
applyStencil() {
NEEDLE_render_objects.applyStencil(this);
}
/** Apply the settings of this renderer to the given object
* Settings include shadow casting and receiving (e.g. this.receiveShadows, this.shadowCastingMode)
*/
applySettings(go: Object3D) {
go.receiveShadow = this.receiveShadows;
if (this.shadowCastingMode == ShadowCastingMode.On) {
go.castShadow = true;
}
else go.castShadow = false;
}
private _reflectionProbe: ReflectionProbe | null = null;
private updateReflectionProbe() {
// handle reflection probe
this._reflectionProbe = null;
if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off) {
// update the reflection probe right before rendering
// if we do it immediately the reflection probe might not be enabled yet
// (since this method is called from onEnable)
this.startCoroutine(this._updateReflectionProbe(), FrameEvent.LateUpdate);
this._probeAnchorLastFrame = this.probeAnchor;
}
}
private *_updateReflectionProbe() {
const obj = this.probeAnchor || this.gameObject;
const isAnchor = this.probeAnchor ? true : false;
this._reflectionProbe = ReflectionProbe.get(obj, this.context, isAnchor, this.probeAnchor);
}
private setVisibility(visible: boolean) {
if (!this.isMultiMaterialObject(this.gameObject)) {
setCustomVisibility(this.gameObject, visible);
}
else {
for (const ch of this.gameObject.children) {
if (this.isMeshOrSkinnedMesh(ch)) {
setCustomVisibility(ch, visible);
}
}
}
}
private isMultiMaterialObject(obj: Object3D) {
return obj.type === "Group";
}
private isMeshOrSkinnedMesh(obj: Object3D): obj is Mesh | SkinnedMesh {
return obj.type === "Mesh" || obj.type === "SkinnedMesh";
}
}
export class MeshRenderer extends Renderer {
}
export class SkinnedMeshRenderer extends MeshRenderer {
private _needUpdateBoundingSphere = false;
// private _lastWorldPosition = new Vector3();
awake() {
super.awake();
if (debugskinnedmesh) console.log("SkinnedMeshRenderer for \"" + this.name + "\"", this);
// disable skinned mesh occlusion because of https://github.com/mrdoob/js/issues/14499
this.allowOcclusionWhenDynamic = false;
for (const mesh of this.sharedMeshes) {
// If we don't do that here the bounding sphere matrix used for raycasts will be wrong. Not sure *why* this is necessary
mesh.parent?.updateWorldMatrix(false, true);
this.markBoundsDirty();
}
}
onAfterRender(): void {
super.onAfterRender();
// this.gameObject.parent.position.x += Math.sin(this.context.time.time) * .01;
// if (this.gameObject instanceof SkinnedMesh && this.gameObject.geometry.boundingSphere) {
// const bounds = this.gameObject.geometry.boundingSphere;
// const worldpos = getTempVector().setFromMatrixPosition(this.gameObject.matrixWorld);
// if (worldpos.distanceTo(this._lastWorldPosition) > bounds.radius) {
// this._lastWorldPosition.copy(worldpos);
// this.markBoundsDirty();
// };
// }
if (this._needUpdateBoundingSphere) {
for (const mesh of this.sharedMeshes) {
if (mesh instanceof SkinnedMesh) {
this._needUpdateBoundingSphere = false;
try {
const geometry = mesh.geometry;
const raycastmesh = getRaycastMesh(mesh);
if (raycastmesh) {
mesh.geometry = raycastmesh;
}
mesh.computeBoundingSphere();
mesh.geometry = geometry;
}
catch (err) {
console.error(`Error updating bounding sphere for ${mesh.name}`, err);
}
}
}
}
// if (this.context.time.frame % 30 === 0) this.markBoundsDirty();
if (debugskinnedmesh) {
for (const mesh of this.sharedMeshes) {
if (mesh instanceof SkinnedMesh && mesh.boundingSphere) {
const tempCenter = getTempVector(mesh.boundingSphere.center).applyMatrix4(mesh.matrixWorld);
Gizmos.DrawWireSphere(tempCenter, mesh.boundingSphere.radius, "red");
}
}
}
}
markBoundsDirty() {
this._needUpdateBoundingSphere = true;
}
}
export enum ShadowCastingMode {
/// <summary>
/// <para>No shadows are cast from this object.</para>
/// </summary>
Off,
/// <summary>
/// <para>Shadows are cast from this object.</para>
/// </summary>
On,
/// <summary>
/// <para>Shadows are cast from this object, treating it as two-sided.</para>
/// </summary>
TwoSided,
/// <summary>
/// <para>Object casts shadows, but is otherwise invisible in the Scene.</para>
/// </summary>
ShadowsOnly,
}