diff --git a/packages/room/src/object/visualization/RoomWindowReflectionState.ts b/packages/room/src/object/visualization/RoomWindowReflectionState.ts index ed1f1ba..f7425e6 100644 --- a/packages/room/src/object/visualization/RoomWindowReflectionState.ts +++ b/packages/room/src/object/visualization/RoomWindowReflectionState.ts @@ -8,6 +8,8 @@ interface IWindowReflectionAvatarState texture: Texture; location: IVector3D; verticalOffset: number; + direction: number; + oppositeTexture: Texture; } export class RoomWindowReflectionState @@ -15,7 +17,7 @@ export class RoomWindowReflectionState private static _avatars: Map = new Map(); private static _updateId: number = 0; - public static setAvatar(id: number, texture: Texture, location: IVector3D, verticalOffset: number = 0): void + public static setAvatar(id: number, texture: Texture, location: IVector3D, verticalOffset: number = 0, direction: number = 0, oppositeTexture: Texture = null): void { if(!texture || !location) return; @@ -27,7 +29,9 @@ export class RoomWindowReflectionState id, texture, location: storedLocation, - verticalOffset + verticalOffset, + direction, + oppositeTexture: (oppositeTexture || texture) }); // Always bump updateId so reflected walk cycles stay frame-synced diff --git a/packages/room/src/object/visualization/avatar/AvatarVisualization.ts b/packages/room/src/object/visualization/avatar/AvatarVisualization.ts index 4a158b6..70ce9c8 100644 --- a/packages/room/src/object/visualization/avatar/AvatarVisualization.ts +++ b/packages/room/src/object/visualization/avatar/AvatarVisualization.ts @@ -1,7 +1,7 @@ import { AlphaTolerance, AvatarAction, AvatarGuideStatus, AvatarSetType, IAdvancedMap, IAvatarEffectListener, IAvatarImage, IAvatarImageListener, IGraphicAsset, IObjectVisualizationData, IRoomGeometry, IRoomObject, IRoomObjectModel, RoomObjectSpriteType, RoomObjectVariable } from '@nitrots/api'; import { GetAssetManager } from '@nitrots/assets'; -import { AdvancedMap } from '@nitrots/utils'; -import { Texture } from 'pixi.js'; +import { AdvancedMap, GetRenderer } from '@nitrots/utils'; +import { Container, RenderTexture, Sprite, Texture } from 'pixi.js'; import { RoomObjectSpriteVisualization } from '../RoomObjectSpriteVisualization'; import { RoomWindowReflectionState } from '../RoomWindowReflectionState'; import { AvatarVisualizationData } from './AvatarVisualizationData'; @@ -77,6 +77,9 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement private _needsUpdate: boolean; private _geometryUpdateCounter: number; private _reflectionVerticalOffset: number; + private _reflectionOppositeTexture: Texture; + private _reflectionOppositeDirection: number; + private _reflectionOppositeBaseTexture: Texture; private _additions: Map; @@ -129,6 +132,9 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement this._needsUpdate = false; this._geometryUpdateCounter = -1; this._reflectionVerticalOffset = 0; + this._reflectionOppositeTexture = null; + this._reflectionOppositeDirection = -1; + this._reflectionOppositeBaseTexture = null; this._additions = new Map(); } @@ -154,6 +160,12 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement if(this._avatarImage) this._avatarImage.dispose(); + if(this._reflectionOppositeTexture) + { + this._reflectionOppositeTexture.destroy(true); + this._reflectionOppositeTexture = null; + } + if(this.object) RoomWindowReflectionState.removeAvatar(this.object.id); this._shadow = null; @@ -1010,6 +1022,29 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement this.clearAvatar(); } + private cloneTexture(texture: Texture): Texture + { + if(!texture) return null; + + const width = Math.max(1, Math.ceil(texture.width)); + const height = Math.max(1, Math.ceil(texture.height)); + const target = RenderTexture.create({ width, height }); + const sprite = new Sprite(texture); + const container = new Container(); + + container.addChild(sprite); + + GetRenderer().render({ + target, + container, + clear: true + }); + + container.destroy({ children: true }); + + return target; + } + private updateWindowReflectionSource(): void { if(!this.object) return; @@ -1018,7 +1053,45 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement if(sprite?.texture) { - RoomWindowReflectionState.setAvatar(this.object.id, sprite.texture, this.object.getLocation(), this._reflectionVerticalOffset); + const currentDirection = this._avatarImage?.getDirection(); + let oppositeTexture = sprite.texture; + + if((currentDirection !== undefined) && this._avatarImage) + { + const oppositeDirection = ((currentDirection + 4) % 8); + + if(oppositeDirection !== currentDirection) + { + const highlightEnabled = ((this.object.model.getValue(RoomObjectVariable.FIGURE_HIGHLIGHT_ENABLE) === 1) && (this.object.model.getValue(RoomObjectVariable.FIGURE_HIGHLIGHT) === 1)); + + this._avatarImage.setDirection(AvatarSetType.FULL, oppositeDirection); + + const renderedOpposite = (this._avatarImage.processAsTexture(AvatarSetType.FULL, highlightEnabled) || sprite.texture); + + if((this._reflectionOppositeDirection !== currentDirection) || (this._reflectionOppositeBaseTexture !== sprite.texture) || !this._reflectionOppositeTexture) + { + if(this._reflectionOppositeTexture) + { + this._reflectionOppositeTexture.destroy(true); + this._reflectionOppositeTexture = null; + } + + this._reflectionOppositeTexture = this.cloneTexture(renderedOpposite); + this._reflectionOppositeDirection = currentDirection; + this._reflectionOppositeBaseTexture = sprite.texture; + } + + oppositeTexture = (this._reflectionOppositeTexture || renderedOpposite); + + // Restore the live avatar direction and refresh the current texture so + // movement updates do not keep showing the opposite-facing texture. + this._avatarImage.setDirection(AvatarSetType.FULL, currentDirection); + + sprite.texture = (this._avatarImage.processAsTexture(AvatarSetType.FULL, highlightEnabled) || sprite.texture); + } + } + + RoomWindowReflectionState.setAvatar(this.object.id, sprite.texture, this.object.getLocation(), this._reflectionVerticalOffset, this.object.getDirection().x, oppositeTexture); return; } @@ -1044,6 +1117,15 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement this._cachedAvatars.reset(); this._cachedAvatarEffects.reset(); + if(this._reflectionOppositeTexture) + { + this._reflectionOppositeTexture.destroy(true); + this._reflectionOppositeTexture = null; + } + + this._reflectionOppositeDirection = -1; + this._reflectionOppositeBaseTexture = null; + this._avatarImage = null; if(this.object) RoomWindowReflectionState.removeAvatar(this.object.id); diff --git a/packages/room/src/object/visualization/room/RoomPlane.ts b/packages/room/src/object/visualization/room/RoomPlane.ts index 4aeef53..84cb8f5 100644 --- a/packages/room/src/object/visualization/room/RoomPlane.ts +++ b/packages/room/src/object/visualization/room/RoomPlane.ts @@ -91,8 +91,8 @@ export class RoomPlane implements IRoomPlane private _windowMasks: { leftSideLoc: number; rightSideLoc: number }[] = []; private _lastWindowReflectionUpdateId: number = -1; private _windowReflectionFirstSeenAt: Map = new Map(); - private _windowReflectionLastVisible: Map = new Map(); - private _windowReflectionFadeOut: Map = new Map(); + private _windowReflectionLastVisible: Map = new Map(); + private _windowReflectionFadeOut: Map = new Map(); constructor(origin: IVector3D, location: IVector3D, leftSide: IVector3D, rightSide: IVector3D, type: number, usesMask: boolean, secondaryNormals: IVector3D[], randomSeed: number, textureOffsetX: number = 0, textureOffsetY: number = 0, textureMaxX: number = 0, textureMaxY: number = 0) { @@ -884,7 +884,7 @@ export class RoomPlane implements IRoomPlane const container = new Container(); const visibleAvatarIds = new Set(); - const addReflectionSprite = (texture: Texture, location: IVector3D, alpha: number, verticalOffset: number = 0): boolean => { + const addReflectionSprite = (texture: Texture, oppositeTexture: Texture, location: IVector3D, alpha: number, verticalOffset: number = 0, direction: number = 0, avatarId: number = -1): boolean => { if(!texture?.source || texture.source.destroyed || !texture.source.style || !location || alpha < 0) return false; const relative = Vector3d.dif(location, this._location); @@ -908,9 +908,33 @@ export class RoomPlane implements IRoomPlane const x = (canvasWidth - ((canvasWidth * leftSideLoc) / this._leftSide.length)); const y = (canvasHeight - ((canvasHeight * rightSideLoc) / this._rightSide.length)) + verticalOffset; - const sprite = new Sprite(texture); + const toPlaneX = (this._location.x - location.x); + const toPlaneY = (this._location.y - location.y); + const toPlaneLength = Math.hypot(toPlaneX, toPlaneY); + + const facingRadians = ((((direction - 90) % 360) + 360) % 360) * (Math.PI / 180); + const facingX = Math.cos(facingRadians); + const facingY = Math.sin(facingRadians); + const facingWindow = (toPlaneLength > 0.001) + ? (((facingX * toPlaneX) + (facingY * toPlaneY)) / toPlaneLength) > 0.5 + : false; + + const deltaLeft = Math.abs(closestMask.mask.leftSideLoc - leftSideLoc); + const deltaRight = Math.abs(closestMask.mask.rightSideLoc - rightSideLoc); + + const isInFrontOfWindow = ((closestMask.score <= 2) && ((deltaLeft <= 0.9) || (deltaRight <= 0.9))); + const shouldMirror = isInFrontOfWindow; + + const normal2DLength = Math.hypot(this._normal.x, this._normal.y); + const normalX = (normal2DLength > 0.0001) ? (this._normal.x / normal2DLength) : 0; + const normalY = (normal2DLength > 0.0001) ? (this._normal.y / normal2DLength) : 0; + const normalFacingDot = Math.abs((facingX * normalX) + (facingY * normalY)); + const useOppositeTexture = (shouldMirror && (normalFacingDot > 0.7)); + + const sprite = new Sprite((useOppositeTexture ? (oppositeTexture || texture) : texture)); sprite.anchor.set(0.5, 1); sprite.position.set(Math.trunc(x), Math.trunc(y)); + sprite.scale.set(1, 1); sprite.tint = 0xCFE3FF; sprite.alpha = alpha; @@ -935,7 +959,7 @@ export class RoomPlane implements IRoomPlane const progress = (elapsed / fadeDurationMs); const alpha = (0.4 * progress); - if(!addReflectionSprite(avatar.texture, avatar.location, alpha, avatar.verticalOffset || 0)) continue; + if(!addReflectionSprite(avatar.texture, avatar.oppositeTexture, avatar.location, alpha, avatar.verticalOffset || 0, avatar.direction || 0, avatar.id)) continue; if(!this._windowReflectionFirstSeenAt.has(avatar.id)) this._windowReflectionFirstSeenAt.set(avatar.id, firstSeenAt); @@ -947,8 +971,10 @@ export class RoomPlane implements IRoomPlane this._windowReflectionLastVisible.set(avatar.id, { texture: avatar.texture, + oppositeTexture: avatar.oppositeTexture, location: storedLocation, - verticalOffset: avatar.verticalOffset || 0 + verticalOffset: avatar.verticalOffset || 0, + direction: avatar.direction || 0 }); } @@ -966,8 +992,10 @@ export class RoomPlane implements IRoomPlane this._windowReflectionFadeOut.set(id, { texture: lastVisible.texture, + oppositeTexture: lastVisible.oppositeTexture, location: lastVisible.location, verticalOffset: lastVisible.verticalOffset, + direction: lastVisible.direction, startedAt: now }); @@ -988,7 +1016,7 @@ export class RoomPlane implements IRoomPlane const alpha = (0.4 * (1 - (elapsed / fadeDurationMs))); - if(!addReflectionSprite(fadeOut.texture, fadeOut.location, alpha, fadeOut.verticalOffset)) this._windowReflectionFadeOut.delete(id); + if(!addReflectionSprite(fadeOut.texture, fadeOut.oppositeTexture, fadeOut.location, alpha, fadeOut.verticalOffset, fadeOut.direction, id)) this._windowReflectionFadeOut.delete(id); } if(!container.children.length)