UNPKG

@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.

494 lines • 17.4 kB
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 { BufferAttribute, BufferGeometry, Color, DoubleSide, Material, Mesh, MeshBasicMaterial, NearestFilter, SRGBColorSpace, Texture } from "three"; import { serializable, serializeable } from "../engine/engine_serialization_decorator.js"; import { getParam } from "../engine/engine_utils.js"; import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js"; import { RGBAColor } from "../engine/js-extensions/index.js"; import { Behaviour } from "./Component.js"; const debug = getParam("debugspriterenderer"); const showWireframe = getParam("wireframe"); /** * @internal */ class SpriteUtils { static cache = {}; static getOrCreateGeometry(sprite) { if (sprite.__cached_geometry) return sprite.__cached_geometry; if (sprite.guid) { if (SpriteUtils.cache[sprite.guid]) { if (debug) console.log("Take cached geometry for sprite", sprite.guid); return SpriteUtils.cache[sprite.guid]; } } const geo = new BufferGeometry(); sprite.__cached_geometry = geo; const vertices = new Float32Array(sprite.triangles.length * 3); const uvs = new Float32Array(sprite.triangles.length * 2); for (let i = 0; i < sprite.triangles.length; i += 1) { const index = sprite.triangles[i]; vertices[i * 3] = -sprite.vertices[index].x; vertices[i * 3 + 1] = sprite.vertices[index].y; vertices[i * 3 + 2] = 0; const uv = sprite.uv[index]; uvs[i * 2] = uv.x; uvs[i * 2 + 1] = 1 - uv.y; } geo.setAttribute("position", new BufferAttribute(vertices, 3)); geo.setAttribute("uv", new BufferAttribute(uvs, 2)); if (sprite.guid) this.cache[sprite.guid] = geo; if (debug) console.log("Built sprite geometry", sprite, geo); return geo; } } /// <summary> /// <para>SpriteRenderer draw mode.</para> /// </summary> export var SpriteDrawMode; (function (SpriteDrawMode) { /// <summary> /// <para>Displays the full sprite.</para> /// </summary> SpriteDrawMode[SpriteDrawMode["Simple"] = 0] = "Simple"; /// <summary> /// <para>The SpriteRenderer will render the sprite as a 9-slice image where the corners will remain constant and the other sections will scale.</para> /// </summary> SpriteDrawMode[SpriteDrawMode["Sliced"] = 1] = "Sliced"; /// <summary> /// <para>The SpriteRenderer will render the sprite as a 9-slice image where the corners will remain constant and the other sections will tile.</para> /// </summary> SpriteDrawMode[SpriteDrawMode["Tiled"] = 2] = "Tiled"; })(SpriteDrawMode || (SpriteDrawMode = {})); class Vec2 { x; y; } function updateTextureIfNecessary(tex) { if (!tex) return; if (tex.colorSpace != SRGBColorSpace) { tex.colorSpace = SRGBColorSpace; tex.needsUpdate = true; } if (tex.minFilter == NearestFilter && tex.magFilter == NearestFilter) { tex.anisotropy = 1; tex.needsUpdate = true; } } /** * A sprite is a mesh that represents a 2D image. Used by the {@link SpriteRenderer} to render 2D images in the scene. * @summary 2D image renderer * @category Rendering * @group Components */ export class Sprite { constructor(texture) { if (texture) { this.texture = texture; this.triangles = [0, 1, 2, 0, 2, 3]; this.uv = [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }, { x: 0, y: 1 }]; this.vertices = [{ x: -.5, y: -.5 }, { x: .5, y: -.5 }, { x: .5, y: .5 }, { x: -.5, y: .5 }]; } } guid; texture; triangles; uv; vertices; /** @internal */ __cached_geometry; /** * The mesh that represents the sprite */ get mesh() { if (!this._mesh) { this._mesh = new Mesh(SpriteUtils.getOrCreateGeometry(this), this.material); } return this._mesh; } _mesh; /** * The material used to render the sprite */ get material() { if (!this._material) { if (this.texture) { updateTextureIfNecessary(this.texture); } this._material = new MeshBasicMaterial({ map: this.texture, color: 0xffffff, side: DoubleSide, transparent: true }); } return this._material; } _material; /** * The geometry of the sprite that can be used to create a mesh */ getGeometry() { return SpriteUtils.getOrCreateGeometry(this); } } __decorate([ serializable() ], Sprite.prototype, "guid", void 0); __decorate([ serializable(Texture) ], Sprite.prototype, "texture", void 0); __decorate([ serializeable() ], Sprite.prototype, "triangles", void 0); __decorate([ serializeable() ], Sprite.prototype, "uv", void 0); __decorate([ serializeable() ], Sprite.prototype, "vertices", void 0); const $spriteTexOwner = Symbol("spriteOwner"); /** * @category Sprites */ export class SpriteSheet { sprites; constructor() { this.sprites = []; } } __decorate([ serializable(Sprite) ], SpriteSheet.prototype, "sprites", void 0); /** * Used by the {@link SpriteRenderer} to hold the sprite sheet and the currently active sprite index. * * @category Sprites */ export class SpriteData { static create() { const i = new SpriteData(); i.spriteSheet = new SpriteSheet(); return i; } // we don't assign anything here because it's used by the serialization system. // there's currently a limitation in the serializer when e.g. spriteSheet is already assigned it will not be overriden by the serializer // hence the spriteSheet field is undefined by default constructor() { } clone() { const i = new SpriteData(); i.index = this.index; i.spriteSheet = this.spriteSheet; return i; } /** * Set the sprite to be rendered in the currently assigned sprite sheet at the currently active index {@link index} */ set sprite(sprite) { if (!sprite) { return; } if (!this.spriteSheet) { this.spriteSheet = new SpriteSheet(); this.spriteSheet.sprites = [sprite]; this.index = 0; } else { if (this.index === null || this.index === undefined) this.index = 0; this.spriteSheet.sprites[this.index] = sprite; } } /** The currently active sprite */ get sprite() { if (!this.spriteSheet) return undefined; return this.spriteSheet.sprites[this.index]; } /** * The spritesheet holds all sprites that can be rendered by the sprite renderer */ spriteSheet; /** * The index of the sprite to be rendered in the currently assigned sprite sheet */ index = 0; update(material) { if (!this.spriteSheet) return; const index = this.index; if (index < 0 || index >= this.spriteSheet.sprites.length) return; const sprite = this.spriteSheet.sprites[index]; const tex = sprite?.texture; if (!tex) return; updateTextureIfNecessary(tex); if (!sprite["__hasLoadedProgressive"]) { sprite["__hasLoadedProgressive"] = true; const previousTexture = tex; NEEDLE_progressive.assignTextureLOD(tex, 0).then(res => { if (res instanceof Texture) { sprite.texture = res; const shouldUpdateInMaterial = material?.["map"] === previousTexture; if (shouldUpdateInMaterial) { material["map"] = res; material.needsUpdate = true; } } }); } } } __decorate([ serializable(SpriteSheet) ], SpriteData.prototype, "spriteSheet", void 0); __decorate([ serializable() ], SpriteData.prototype, "index", void 0); /** * The sprite renderer renders a sprite on a GameObject using an assigned spritesheet ({@link SpriteData}). * * - Example: https://engine.needle.tools/samples/spritesheet-animation * * @summary Renders 2D images from a sprite sheet * @category Rendering * @group Components */ export class SpriteRenderer extends Behaviour { /** @internal The draw mode of the sprite renderer */ drawMode = SpriteDrawMode.Simple; /** @internal Used when drawMode is set to Tiled */ size = { x: 1, y: 1 }; color; /** * The material that is used to render the sprite */ sharedMaterial; // additional data transparent = true; cutoutThreshold = 0; castShadows = false; renderOrder = 0; toneMapped = true; /** * Assign a new texture to the currently active sprite */ set texture(value) { if (!this._spriteSheet) return; const currentSprite = this._spriteSheet.spriteSheet?.sprites[this.spriteIndex]; if (!currentSprite) return; currentSprite.texture = value; this.updateSprite(); } /** * Add a new sprite to the currently assigned sprite sheet. The sprite will be added to the end of the sprite sheet. * Note that the sprite will not be rendered by default - set the `spriteIndex` to the index of the sprite to be rendered. * @param sprite The sprite to be added * @returns The index of the sprite in the sprite sheet * @example * ```typescript * const spriteRenderer = gameObject.addComponent(SpriteRenderer); * const index = spriteRenderer.addSprite(mySprite); * if(index >= 0) * spriteRenderer.spriteIndex = index; * ``` */ addSprite(sprite, setActive = false) { if (!this._spriteSheet) { this._spriteSheet = SpriteData.create(); } if (!this._spriteSheet.spriteSheet) return -1; this._spriteSheet.spriteSheet?.sprites.push(sprite); const index = this._spriteSheet.spriteSheet?.sprites.length - 1; if (setActive) { this.spriteIndex = index; } return index; } /** * Get the currently active sprite */ get sprite() { return this._spriteSheet; } /** * Set the sprite to be rendered in the currently assigned sprite sheet at the currently active index {@link spriteIndex} */ set sprite(value) { if (value === this._spriteSheet) return; if (typeof value === "number") { // the value if interpolated is sometimes *slightly* off (e.g. 0.999999 or 1.000001) so we round it const index = Math.round(value); if (debug) console.log("[SpriteSheet] Set index to " + index + " (was " + this.spriteIndex + ")", value); this.spriteIndex = index; } else if (value instanceof Sprite) { if (!this._spriteSheet) { this._spriteSheet = SpriteData.create(); } if (this._spriteSheet.sprite != value) { this._spriteSheet.sprite = value; } this.updateSprite(); } else if (value != this._spriteSheet) { this._spriteSheet = value; this.updateSprite(); } } /** * Set the index of the sprite to be rendered in the currently assigned sprite sheet */ set spriteIndex(value) { if (!this._spriteSheet) return; this._spriteSheet.index = value; this.updateSprite(); } get spriteIndex() { return this._spriteSheet?.index ?? 0; } /** * Get the number of sprites in the currently assigned sprite sheet */ get spriteFrames() { return this._spriteSheet?.spriteSheet?.sprites.length ?? 0; } _spriteSheet; _currentSprite; /** @internal */ awake() { this._currentSprite = undefined; if (!this._spriteSheet) { this._spriteSheet = SpriteData.create(); } else { // Ensure each SpriteRenderer has a unique spritesheet instance for cases where sprite renderer are cloned at runtime and then different sprites are assigned to each instance this._spriteSheet = this._spriteSheet.clone(); } if (debug) { console.log("Awake", this.name, this, this.sprite); } } /** @internal */ start() { if (!this._currentSprite) this.updateSprite(); else if (this.gameObject) this.gameObject.add(this._currentSprite); } /** * Update the sprite. Modified properties will be applied to the sprite mesh. This method is called automatically when the sprite is changed. * @param force If true, the sprite will be forced to update. * @returns True if the sprite was updated successfully */ updateSprite(force = false) { if (!this.__didAwake && !force) return false; const data = this._spriteSheet; if (!data?.spriteSheet?.sprites) { console.warn("SpriteRenderer has no data or spritesheet assigned..."); return false; } const sprite = data.spriteSheet.sprites[this.spriteIndex]; if (!sprite) { if (debug) console.warn("Sprite not found", this.spriteIndex, data.spriteSheet.sprites); return false; } if (!this._currentSprite) { const mat = new MeshBasicMaterial({ color: 0xffffff, side: DoubleSide }); if (showWireframe) mat.wireframe = true; if (this.color) { if (!mat["color"]) mat["color"] = new Color(); mat["color"].copy(this.color); mat["opacity"] = this.color.alpha; } mat.transparent = true; mat.toneMapped = this.toneMapped; mat.depthWrite = false; if (sprite.texture && !mat.wireframe) { let tex = sprite.texture; // the sprite renderer modifies the texture offset and scale // so we need to clone the texture // if the same texture is used multiple times if (tex[$spriteTexOwner] !== undefined && tex[$spriteTexOwner] !== this && this.spriteFrames > 1) { tex = sprite.texture = tex.clone(); } tex[$spriteTexOwner] = this; mat["map"] = tex; } this.sharedMaterial = mat; this._currentSprite = new Mesh(SpriteUtils.getOrCreateGeometry(sprite), mat); this._currentSprite.renderOrder = Math.round(this.renderOrder); NEEDLE_progressive.assignTextureLOD(mat, 0); } else { this._currentSprite.geometry = SpriteUtils.getOrCreateGeometry(sprite); this._currentSprite.material["map"] = sprite.texture; } if (this._currentSprite.parent !== this.gameObject) { if (this.drawMode === SpriteDrawMode.Tiled) this._currentSprite.scale.set(this.size.x, this.size.y, 1); if (this.gameObject) this.gameObject.add(this._currentSprite); } if (this._currentSprite) { this._currentSprite.layers.set(this.layer); } if (this.sharedMaterial) { this.sharedMaterial.alphaTest = this.cutoutThreshold; this.sharedMaterial.transparent = this.transparent; } this._currentSprite.castShadow = this.castShadows; data?.update(this.sharedMaterial); return true; } } __decorate([ serializable() ], SpriteRenderer.prototype, "drawMode", void 0); __decorate([ serializable(Vec2) ], SpriteRenderer.prototype, "size", void 0); __decorate([ serializable(RGBAColor) ], SpriteRenderer.prototype, "color", void 0); __decorate([ serializable(Material) ], SpriteRenderer.prototype, "sharedMaterial", void 0); __decorate([ serializable() ], SpriteRenderer.prototype, "transparent", void 0); __decorate([ serializable() ], SpriteRenderer.prototype, "cutoutThreshold", void 0); __decorate([ serializable() ], SpriteRenderer.prototype, "castShadows", void 0); __decorate([ serializable() ], SpriteRenderer.prototype, "renderOrder", void 0); __decorate([ serializable() ], SpriteRenderer.prototype, "toneMapped", void 0); __decorate([ serializable(SpriteData) ], SpriteRenderer.prototype, "sprite", null); //# sourceMappingURL=SpriteRenderer.js.map