cleanup, name tag
This commit is contained in:
parent
19ea3d22ab
commit
d25086632f
|
@ -15,8 +15,8 @@ export class ThirdPersonCamera {
|
|||
private prevMousePos = new Vector2();
|
||||
|
||||
private angleAroundPlayer = 180;
|
||||
private pitch = -45;
|
||||
private distance = 6;
|
||||
private pitch = -24;
|
||||
private distance = 8;
|
||||
|
||||
private panning = false;
|
||||
private pinching = false;
|
||||
|
@ -25,10 +25,14 @@ export class ThirdPersonCamera {
|
|||
|
||||
private _moveFn?: (x: number, y: number) => void;
|
||||
|
||||
public minZoom = 2;
|
||||
public maxZoom = 32;
|
||||
|
||||
constructor(
|
||||
private camera: PerspectiveCamera,
|
||||
private target: Object3D,
|
||||
private eventTarget: HTMLElement
|
||||
private eventTarget: HTMLElement,
|
||||
private cameraOrignOffset = new Vector3(0, 4.75, 0)
|
||||
) {}
|
||||
|
||||
private dragEvent = (x: number, y: number) => {
|
||||
|
@ -44,12 +48,12 @@ export class ThirdPersonCamera {
|
|||
this.angleAroundPlayer + ((offset.x * 0.3) % 360);
|
||||
}
|
||||
|
||||
this.pitch = clamp(this.pitch + offset.y * 0.3, -90, 90);
|
||||
this.pitch = clamp(this.pitch + offset.y * 0.3, -89.9, 89.9);
|
||||
this.calculateCameraOffset();
|
||||
}
|
||||
};
|
||||
|
||||
events: Record<string, (...arg: any[]) => void> = {
|
||||
private events: Record<string, (...arg: any[]) => void> = {
|
||||
contextmenu: (e: MouseEvent) => e.preventDefault(),
|
||||
mousedown: (e: MouseEvent) => {
|
||||
if (e.button === 2) {
|
||||
|
@ -76,7 +80,7 @@ export class ThirdPersonCamera {
|
|||
mousemove: (e: MouseEvent) => this.dragEvent(e.clientX, e.clientY),
|
||||
wheel: (e: WheelEvent) => {
|
||||
e.deltaY < 0 ? (this.distance /= 1.2) : (this.distance *= 1.2);
|
||||
this.distance = clamp(this.distance, 3, 20);
|
||||
this.distance = clamp(this.distance, this.minZoom, this.maxZoom);
|
||||
this.calculateCameraOffset();
|
||||
},
|
||||
// mobile
|
||||
|
@ -102,7 +106,7 @@ export class ThirdPersonCamera {
|
|||
if (this.previousPinchLength) {
|
||||
const delta = pinchLength / this.previousPinchLength;
|
||||
delta > 0 ? (this.distance *= delta) : (this.distance /= delta);
|
||||
this.distance = clamp(this.distance, 3, 20);
|
||||
this.distance = clamp(this.distance, this.minZoom, this.maxZoom);
|
||||
this.calculateCameraOffset();
|
||||
}
|
||||
|
||||
|
@ -125,7 +129,9 @@ export class ThirdPersonCamera {
|
|||
|
||||
initialize() {
|
||||
Object.keys(this.events).forEach((key) => {
|
||||
this.eventTarget.addEventListener(key, this.events[key]);
|
||||
this.eventTarget.addEventListener(key, this.events[key], {
|
||||
passive: false,
|
||||
});
|
||||
});
|
||||
this.calculateCameraOffset();
|
||||
}
|
||||
|
@ -171,12 +177,13 @@ export class ThirdPersonCamera {
|
|||
|
||||
private getTargetOffset(): Vector3 {
|
||||
const offset = this.offsetFromPlayer.clone();
|
||||
offset.add(this.cameraOrignOffset);
|
||||
offset.add(this.target.position);
|
||||
return offset;
|
||||
}
|
||||
|
||||
private getTargetLookAt(): Vector3 {
|
||||
const offset = new Vector3(0, 4.75, 0);
|
||||
const offset = this.cameraOrignOffset.clone();
|
||||
offset.add(this.target.position);
|
||||
return offset;
|
||||
}
|
||||
|
|
|
@ -34,9 +34,9 @@ export class Game extends Engine {
|
|||
}
|
||||
|
||||
async loadLevel(path: string) {
|
||||
this.events.emit('reset');
|
||||
const data = await assetManager.loadJsonData(path);
|
||||
await this.getComponent(LevelComponent).deserializeLevelSave(data);
|
||||
|
||||
this.events.emit('initialized');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,7 @@ import {
|
|||
} from '@freeblox/engine';
|
||||
import { GameEvents } from '../types/events';
|
||||
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
import { Object3D, Vector3 } from 'three';
|
||||
import { Vector3 } from 'three';
|
||||
import { ThirdPersonCamera } from './camera';
|
||||
|
||||
/**
|
||||
|
@ -17,7 +16,6 @@ import { ThirdPersonCamera } from './camera';
|
|||
*/
|
||||
export class GameplayComponent extends EngineComponent {
|
||||
public name = GameplayComponent.name;
|
||||
public events = new EventEmitter<GameEvents>();
|
||||
public world!: World;
|
||||
public cleanUpEvents?: () => void;
|
||||
|
||||
|
@ -39,7 +37,6 @@ export class GameplayComponent extends EngineComponent {
|
|||
override initialize(): void {
|
||||
this.world = this.renderer.scene.getObjectByName('World') as World;
|
||||
this.cleanUpEvents = this.bindEvents();
|
||||
this.loadCharacter('Diamond');
|
||||
}
|
||||
|
||||
override update(delta: number): void {
|
||||
|
@ -67,7 +64,7 @@ export class GameplayComponent extends EngineComponent {
|
|||
}
|
||||
}
|
||||
|
||||
override cleanUp(): void {
|
||||
override dispose(): void {
|
||||
this.cleanUpEvents?.();
|
||||
}
|
||||
|
||||
|
@ -122,11 +119,17 @@ export class GameplayComponent extends EngineComponent {
|
|||
}
|
||||
};
|
||||
|
||||
const initializedEvent = () => {
|
||||
this.loadCharacter('Diamond');
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', keyDownEvent);
|
||||
window.addEventListener('keyup', keyUpEvent);
|
||||
this.events.addListener('initialized', initializedEvent);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', keyDownEvent);
|
||||
window.removeEventListener('keyup', keyUpEvent);
|
||||
this.events.removeEventListener('initialized', initializedEvent);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ export class HistoryComponent extends EngineComponent {
|
|||
|
||||
update(delta: number): void {}
|
||||
|
||||
cleanUp(): void {
|
||||
dispose(): void {
|
||||
this.cleanUpEvents?.call(this);
|
||||
this.history.length = 0;
|
||||
this.restory.length = 0;
|
||||
|
|
|
@ -21,7 +21,7 @@ export class ShortcutsComponent extends EngineComponent {
|
|||
|
||||
update(delta: number): void {}
|
||||
|
||||
cleanUp(): void {
|
||||
dispose(): void {
|
||||
this.cleanUpEvents?.call(this);
|
||||
}
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
this.viewHelper?.render(this.renderer.renderer);
|
||||
}
|
||||
|
||||
cleanUp(): void {
|
||||
dispose(): void {
|
||||
this.removeFromScene(this.renderer.scene);
|
||||
this.cleanUpEvents?.call(this);
|
||||
this.viewHelper?.dispose();
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './utils';
|
|
@ -0,0 +1,227 @@
|
|||
import { CanvasTexture, LinearFilter, ClampToEdgeWrapping } from 'three';
|
||||
|
||||
export function rgbToHex(r: number, g: number, b: number): number {
|
||||
return (1 << 24) + (r << 16) + (g << 8) + b;
|
||||
}
|
||||
|
||||
type RadiusQuad = { tl: number; tr: number; br: number; bl: number };
|
||||
|
||||
export interface CanvasUtilsOptions {
|
||||
fill?: boolean;
|
||||
backgroundColor?: string;
|
||||
foregroundColor?: string;
|
||||
rounded?: boolean;
|
||||
roundedRadius?: number;
|
||||
textBorderColor?: string;
|
||||
textBorderSize?: number;
|
||||
textShadowBlur?: number;
|
||||
}
|
||||
|
||||
export class CanvasUtils {
|
||||
constructor(private options?: CanvasUtilsOptions) {}
|
||||
|
||||
public createTextCanvas(
|
||||
text: string | string[],
|
||||
bold = true,
|
||||
fontSize = 16,
|
||||
padding = 4
|
||||
): { texture: CanvasTexture; width: number; height: number } {
|
||||
const ctx = document.createElement('canvas').getContext('2d')!;
|
||||
const font = `${fontSize}px${bold ? ' bold' : ''} sans`;
|
||||
|
||||
const lines = Array.isArray(text) ? text : [text];
|
||||
const lineWidths: number[] = [];
|
||||
let longestLine = 0;
|
||||
|
||||
// Measure the text bounds
|
||||
ctx.font = font;
|
||||
lines.forEach((line) => {
|
||||
const lineWidth = ctx.measureText(line).width;
|
||||
if (longestLine < lineWidth) {
|
||||
longestLine = lineWidth;
|
||||
}
|
||||
lineWidths.push(lineWidth);
|
||||
});
|
||||
|
||||
const width = longestLine + padding * 2;
|
||||
const textHeight = fontSize * lines.length;
|
||||
const height = textHeight + padding * 2;
|
||||
|
||||
// Resize canvas
|
||||
ctx.canvas.width = width;
|
||||
ctx.canvas.height = height;
|
||||
|
||||
// Set text parameters
|
||||
ctx.font = font;
|
||||
ctx.textAlign = 'center';
|
||||
|
||||
// Draw background
|
||||
if (this.options?.fill ?? true) {
|
||||
ctx.fillStyle = this.options?.backgroundColor || '#fff';
|
||||
if (this.options?.rounded) {
|
||||
CanvasUtils.roundRect(
|
||||
ctx,
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height,
|
||||
this.options?.roundedRadius || 4,
|
||||
true,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
// Scale the text to fit within the canvas
|
||||
const scaleFactor = Math.min(1, width / longestLine);
|
||||
ctx.translate(
|
||||
Math.floor(width / 2 - padding) + 0.5,
|
||||
Math.floor(padding + fontSize / 2) + 0.5
|
||||
);
|
||||
ctx.scale(scaleFactor, 1);
|
||||
|
||||
// Draw the text
|
||||
|
||||
if (this.options?.textShadowBlur !== undefined) {
|
||||
ctx.shadowColor = this.options?.textBorderColor || '#000';
|
||||
ctx.shadowBlur = this.options?.textShadowBlur;
|
||||
if (this.options?.textBorderSize !== undefined) {
|
||||
ctx.lineWidth = this.options?.textBorderSize;
|
||||
lines.forEach((line, i) => {
|
||||
ctx.strokeText(line, padding, i * fontSize + padding);
|
||||
});
|
||||
}
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
ctx.fillStyle = this.options?.foregroundColor || '#000';
|
||||
lines.forEach((line, i) => {
|
||||
ctx.fillText(line, padding, i * fontSize + padding);
|
||||
});
|
||||
|
||||
// Create texture with appropriate flags
|
||||
const texture = new CanvasTexture(ctx.canvas);
|
||||
texture.minFilter = LinearFilter;
|
||||
texture.wrapS = ClampToEdgeWrapping;
|
||||
texture.wrapT = ClampToEdgeWrapping;
|
||||
|
||||
return { texture, width, height };
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a rounded rectangle using the current state of the canvas.
|
||||
* If you omit the last three params, it will draw a rectangle
|
||||
* outline with a 5 pixel border radius
|
||||
*
|
||||
* https://stackoverflow.com/a/3368118
|
||||
*
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
* @param {Number} x The top left x coordinate
|
||||
* @param {Number} y The top left y coordinate
|
||||
* @param {Number} width The width of the rectangle
|
||||
* @param {Number} height The height of the rectangle
|
||||
* @param {Number} [radius = 5] The corner radius; It can also be an object
|
||||
* to specify different radii for corners
|
||||
* @param {Number} [radius.tl = 0] Top left
|
||||
* @param {Number} [radius.tr = 0] Top right
|
||||
* @param {Number} [radius.br = 0] Bottom right
|
||||
* @param {Number} [radius.bl = 0] Bottom left
|
||||
* @param {Boolean} [fill = false] Whether to fill the rectangle.
|
||||
* @param {Boolean} [stroke = true] Whether to stroke the rectangle.
|
||||
*/
|
||||
public static roundRect(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
radius?: number | RadiusQuad,
|
||||
fill?: boolean,
|
||||
stroke?: boolean
|
||||
) {
|
||||
if (typeof stroke === 'undefined') {
|
||||
stroke = true;
|
||||
}
|
||||
if (typeof radius === 'undefined') {
|
||||
radius = 5;
|
||||
}
|
||||
if (typeof radius === 'number') {
|
||||
radius = <RadiusQuad>{ tl: radius, tr: radius, br: radius, bl: radius };
|
||||
} else {
|
||||
const defaultRadius = <RadiusQuad>{ tl: 0, tr: 0, br: 0, bl: 0 };
|
||||
for (let side in defaultRadius) {
|
||||
const sideKey = side as keyof RadiusQuad;
|
||||
radius[sideKey] = radius[sideKey] || defaultRadius[sideKey];
|
||||
}
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radius.tl, y);
|
||||
ctx.lineTo(x + width - radius.tr, y);
|
||||
ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
|
||||
ctx.lineTo(x + width, y + height - radius.br);
|
||||
ctx.quadraticCurveTo(
|
||||
x + width,
|
||||
y + height,
|
||||
x + width - radius.br,
|
||||
y + height
|
||||
);
|
||||
ctx.lineTo(x + radius.bl, y + height);
|
||||
ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
|
||||
ctx.lineTo(x, y + radius.tl);
|
||||
ctx.quadraticCurveTo(x, y, x + radius.tl, y);
|
||||
ctx.closePath();
|
||||
if (fill) {
|
||||
ctx.fill();
|
||||
}
|
||||
if (stroke) {
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
public static readPixelDataRGB(image: HTMLImageElement): number[] {
|
||||
const array = new Array(image.width * image.height);
|
||||
const ctx = document.createElement('canvas').getContext('2d')!;
|
||||
ctx.canvas.width = image.width;
|
||||
ctx.canvas.height = image.height;
|
||||
ctx.drawImage(image, 0, 0, image.width, image.height);
|
||||
|
||||
// pixel data
|
||||
const data = ctx.getImageData(0, 0, image.width, image.height);
|
||||
for (let x = 0; x < image.width; x++) {
|
||||
for (let y = 0; y < image.height; y++) {
|
||||
const index = x + y * image.width;
|
||||
array[index] = rgbToHex(
|
||||
data.data[index * 4],
|
||||
data.data[index * 4 + 1],
|
||||
data.data[index * 4 + 2]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
public static readPixelDataRScaled(
|
||||
image: HTMLImageElement,
|
||||
scale: number
|
||||
): number[] {
|
||||
const array = new Array(image.width * image.height);
|
||||
const ctx = document.createElement('canvas').getContext('2d')!;
|
||||
ctx.canvas.width = image.width;
|
||||
ctx.canvas.height = image.height;
|
||||
ctx.drawImage(image, 0, 0, image.width, image.height);
|
||||
|
||||
// pixel data
|
||||
const data = ctx.getImageData(0, 0, image.width, image.height);
|
||||
for (let x = 0; x < image.width; x++) {
|
||||
for (let y = 0; y < image.height; y++) {
|
||||
const index = x + y * image.width;
|
||||
array[index] = (data.data[index * 4] * scale) / 255;
|
||||
}
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
}
|
|
@ -38,7 +38,7 @@ export class EnvironmentComponent extends EngineComponent {
|
|||
|
||||
update(delta: number): void {}
|
||||
|
||||
cleanUp(): void {
|
||||
dispose(): void {
|
||||
this.renderer.scene.remove(this.ambient);
|
||||
this.renderer.scene.remove(this.directional);
|
||||
this.cleanUpEvents?.call(this);
|
||||
|
|
|
@ -48,7 +48,7 @@ export class LevelComponent extends EngineComponent {
|
|||
|
||||
update(delta: number): void {}
|
||||
|
||||
cleanUp(): void {
|
||||
dispose(): void {
|
||||
this.cleanUpEvents?.call(this);
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,6 @@ export class LevelComponent extends EngineComponent {
|
|||
public async deserializeLevelSave(save: WorldFile) {
|
||||
// Reset the world
|
||||
this.events.emit('reset');
|
||||
assetManager.freeAll();
|
||||
|
||||
// Load all assets
|
||||
await assetManager.loadAll(save.assets);
|
||||
|
|
|
@ -49,7 +49,7 @@ export class MouseComponent extends EngineComponent {
|
|||
this.mouseButtonsLast = [...this.mouseButtons];
|
||||
}
|
||||
|
||||
cleanUp(): void {
|
||||
dispose(): void {
|
||||
this.cleanUpEvents?.call(this);
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ export class ViewportComponent extends EngineComponent {
|
|||
|
||||
update(dt: number) {}
|
||||
|
||||
cleanUp(): void {
|
||||
dispose(): void {
|
||||
this.cleanUpEvents?.call(this);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import { Renderer } from './renderer';
|
|||
export class Engine extends GameRunner {
|
||||
public lastTick = performance.now();
|
||||
public running = false;
|
||||
public events = new EventEmitter<EngineEvents>();
|
||||
public events!: EventEmitter<EngineEvents>;
|
||||
public render!: Renderer;
|
||||
public element!: HTMLElement;
|
||||
public components: EngineComponent[] = [];
|
||||
|
@ -31,7 +31,7 @@ export class Engine extends GameRunner {
|
|||
override stop(): void {
|
||||
this.running = false;
|
||||
for (const component of this.components) {
|
||||
component.cleanUp();
|
||||
component.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ export class Renderer {
|
|||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
cleanUp() {
|
||||
dispose() {
|
||||
this.renderer.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ import { Ticking } from '../types/ticking';
|
|||
import { EditorProperty } from '../decorators/property';
|
||||
import { MeshPart } from './mesh.object';
|
||||
import { clamp } from 'three/src/math/MathUtils.js';
|
||||
import { NameTag } from './nametag.object';
|
||||
import { CanvasUtils } from '../canvas/utils';
|
||||
|
||||
export type HumanoidBodyPart =
|
||||
| 'Head'
|
||||
|
@ -53,10 +55,20 @@ export class Humanoid extends GameObject implements Ticking {
|
|||
|
||||
public static bodyAnimationNames = ['Idle', 'Walk'];
|
||||
|
||||
public static nameTagBuilder = new CanvasUtils({
|
||||
fill: false,
|
||||
textBorderColor: '#000',
|
||||
foregroundColor: '#fff',
|
||||
textShadowBlur: 2,
|
||||
textBorderSize: 1,
|
||||
});
|
||||
|
||||
private mixer!: AnimationMixer;
|
||||
private idleAction!: AnimationAction;
|
||||
private walkAction!: AnimationAction;
|
||||
|
||||
private nameTag?: NameTag;
|
||||
|
||||
@EditorProperty({ type: Number })
|
||||
get health() {
|
||||
return this._health;
|
||||
|
@ -114,6 +126,8 @@ export class Humanoid extends GameObject implements Ticking {
|
|||
this.idleAction = this.mixer.clipAction(idleClip);
|
||||
this.walkAction = this.mixer.clipAction(walkClip);
|
||||
this.idleAction.play();
|
||||
|
||||
this.createNameTag();
|
||||
}
|
||||
|
||||
setVelocity(velocity: Vector3) {
|
||||
|
@ -173,4 +187,14 @@ export class Humanoid extends GameObject implements Ticking {
|
|||
this.health = 0;
|
||||
this.detach();
|
||||
}
|
||||
|
||||
private createNameTag() {
|
||||
if (this.nameTag) {
|
||||
this.nameTag.dispose();
|
||||
}
|
||||
|
||||
this.nameTag = NameTag.create(Humanoid.nameTagBuilder, this.parent!.name);
|
||||
this.nameTag.position.set(0, 1.5, 0);
|
||||
this.add(this.nameTag);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ export const instancableGameObjects: Record<string, Instancable<GameObject>> = {
|
|||
|
||||
export * from './environment.object';
|
||||
export * from './world.object';
|
||||
export * from './nametag.object';
|
||||
export {
|
||||
Group,
|
||||
Cylinder,
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import { Sprite, CanvasTexture, SpriteMaterial } from 'three';
|
||||
import { CanvasUtils } from '../canvas';
|
||||
import { GameObject } from '..';
|
||||
|
||||
export class NameTag extends GameObject {
|
||||
public objectType = NameTag.name;
|
||||
public virtual = true;
|
||||
public archivable = false;
|
||||
|
||||
public width!: number;
|
||||
public height!: number;
|
||||
private texture!: CanvasTexture;
|
||||
private material!: SpriteMaterial;
|
||||
|
||||
public static create(builder: CanvasUtils, name: string) {
|
||||
const { texture, width, height } = builder.createTextCanvas(
|
||||
name,
|
||||
false,
|
||||
48
|
||||
);
|
||||
const obj = new NameTag();
|
||||
|
||||
obj.texture = texture;
|
||||
obj.width = width;
|
||||
obj.height = height;
|
||||
|
||||
obj.material = new SpriteMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
});
|
||||
|
||||
const label = new Sprite(obj.material);
|
||||
|
||||
const labelBaseScale = 0.01;
|
||||
label.scale.x = width * labelBaseScale;
|
||||
label.scale.y = height * labelBaseScale;
|
||||
|
||||
obj.add(label);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.material.dispose();
|
||||
this.texture.dispose();
|
||||
}
|
||||
}
|
|
@ -24,5 +24,5 @@ export abstract class EngineComponent {
|
|||
/**
|
||||
* Clean up the component
|
||||
*/
|
||||
abstract cleanUp(): void;
|
||||
abstract dispose(): void;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
import {
|
||||
AnimationClip,
|
||||
Bone,
|
||||
Object3D,
|
||||
SkeletonHelper,
|
||||
SkinnedMesh,
|
||||
} from 'three';
|
||||
import { AnimationClip, Bone, Object3D, SkinnedMesh } from 'three';
|
||||
import { assetManager } from '../assets';
|
||||
import { Group } from '../gameobjects/group.object';
|
||||
import { MeshPart } from '../gameobjects/mesh.object';
|
||||
|
|
Loading…
Reference in New Issue