UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

354 lines (351 loc) 11.6 kB
import { EventHandler } from '../core/event-handler.js'; import { Vec2 } from '../core/math/vec2.js'; import { SPRITE_RENDERMODE_SIMPLE, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED } from './constants.js'; import { Mesh } from './mesh.js'; import { Geometry } from './geometry/geometry.js'; /** * @import { GraphicsDevice } from '../platform/graphics/graphics-device.js' * @import { TextureAtlas } from './texture-atlas.js' */ // normals are the same for every mesh const spriteNormals = [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1 ]; // indices are the same for every mesh const spriteIndices = [ 0, 1, 3, 2, 3, 1 ]; /** * A Sprite contains references to one or more frames of a {@link TextureAtlas}. It can be used by * the {@link SpriteComponent} or the {@link ElementComponent} to render a single frame or a sprite * animation. * * @category Graphics */ class Sprite extends EventHandler { /** * Create a new Sprite instance. * * @param {GraphicsDevice} device - The graphics device of the application. * @param {object} [options] - Options for creating the Sprite. * @param {number} [options.pixelsPerUnit] - The number of pixels that map to one PlayCanvas * unit. Defaults to 1. * @param {number} [options.renderMode] - The rendering mode of the sprite. Can be: * * - {@link SPRITE_RENDERMODE_SIMPLE} * - {@link SPRITE_RENDERMODE_SLICED} * - {@link SPRITE_RENDERMODE_TILED} * * Defaults to {@link SPRITE_RENDERMODE_SIMPLE}. * @param {TextureAtlas} [options.atlas] - The texture atlas. Defaults to null. * @param {string[]} [options.frameKeys] - The keys of the frames in the sprite atlas that this * sprite is using. Defaults to null. */ constructor(device, options){ super(); this._device = device; this._pixelsPerUnit = options && options.pixelsPerUnit !== undefined ? options.pixelsPerUnit : 1; this._renderMode = options && options.renderMode !== undefined ? options.renderMode : SPRITE_RENDERMODE_SIMPLE; this._atlas = options && options.atlas !== undefined ? options.atlas : null; this._frameKeys = options && options.frameKeys !== undefined ? options.frameKeys : null; this._meshes = []; // set to true to update multiple // properties without re-creating meshes this._updatingProperties = false; // if true, endUpdate() will re-create meshes when it's called this._meshesDirty = false; if (this._atlas && this._frameKeys) { this._createMeshes(); } } /** * Sets the keys of the frames in the sprite atlas that this sprite is using. * * @type {string[]} */ set frameKeys(value) { this._frameKeys = value; if (this._atlas && this._frameKeys) { if (this._updatingProperties) { this._meshesDirty = true; } else { this._createMeshes(); } } this.fire('set:frameKeys', value); } /** * Gets the keys of the frames in the sprite atlas that this sprite is using. * * @type {string[]} */ get frameKeys() { return this._frameKeys; } /** * Sets the texture atlas. * * @type {TextureAtlas} */ set atlas(value) { if (value === this._atlas) return; if (this._atlas) { this._atlas.off('set:frames', this._onSetFrames, this); this._atlas.off('set:frame', this._onFrameChanged, this); this._atlas.off('remove:frame', this._onFrameRemoved, this); } this._atlas = value; if (this._atlas && this._frameKeys) { this._atlas.on('set:frames', this._onSetFrames, this); this._atlas.on('set:frame', this._onFrameChanged, this); this._atlas.on('remove:frame', this._onFrameRemoved, this); if (this._updatingProperties) { this._meshesDirty = true; } else { this._createMeshes(); } } this.fire('set:atlas', value); } /** * Gets the texture atlas. * * @type {TextureAtlas} */ get atlas() { return this._atlas; } /** * Sets the number of pixels that map to one PlayCanvas unit. * * @type {number} */ set pixelsPerUnit(value) { if (this._pixelsPerUnit === value) return; this._pixelsPerUnit = value; this.fire('set:pixelsPerUnit', value); // simple mode uses pixelsPerUnit to create the mesh so re-create those meshes if (this._atlas && this._frameKeys && this.renderMode === SPRITE_RENDERMODE_SIMPLE) { if (this._updatingProperties) { this._meshesDirty = true; } else { this._createMeshes(); } } } /** * Gets the number of pixels that map to one PlayCanvas unit. * * @type {number} */ get pixelsPerUnit() { return this._pixelsPerUnit; } /** * Sets the rendering mode of the sprite. Can be: * * - {@link SPRITE_RENDERMODE_SIMPLE} * - {@link SPRITE_RENDERMODE_SLICED} * - {@link SPRITE_RENDERMODE_TILED} * * @type {number} */ set renderMode(value) { if (this._renderMode === value) { return; } const prev = this._renderMode; this._renderMode = value; this.fire('set:renderMode', value); // re-create the meshes if we're going from simple to 9-sliced or vice versa if (prev === SPRITE_RENDERMODE_SIMPLE || value === SPRITE_RENDERMODE_SIMPLE) { if (this._atlas && this._frameKeys) { if (this._updatingProperties) { this._meshesDirty = true; } else { this._createMeshes(); } } } } /** * Sets the rendering mode of the sprite. * * @type {number} */ get renderMode() { return this._renderMode; } /** * An array that contains a mesh for each frame. * * @type {Mesh[]} */ get meshes() { return this._meshes; } _createMeshes() { // destroy old meshes const len = this._meshes.length; for(let i = 0; i < len; i++){ const mesh = this._meshes[i]; if (mesh) { mesh.destroy(); } } // clear meshes array const count = this._frameKeys.length; this._meshes = new Array(count); // get function to create meshes const createMeshFunc = this.renderMode === SPRITE_RENDERMODE_SLICED || this._renderMode === SPRITE_RENDERMODE_TILED ? this._create9SliceMesh : this._createSimpleMesh; // create a mesh for each frame in the sprite for(let i = 0; i < count; i++){ const frame = this._atlas.frames[this._frameKeys[i]]; this._meshes[i] = frame ? createMeshFunc.call(this, frame) : null; } this.fire('set:meshes'); } _createSimpleMesh(frame) { const rect = frame.rect; const texWidth = this._atlas.texture.width; const texHeight = this._atlas.texture.height; const w = rect.z / this._pixelsPerUnit; const h = rect.w / this._pixelsPerUnit; const hp = frame.pivot.x; const vp = frame.pivot.y; // positions based on pivot and size of frame const positions = [ -hp * w, -vp * h, 0, (1 - hp) * w, -vp * h, 0, (1 - hp) * w, (1 - vp) * h, 0, -hp * w, (1 - vp) * h, 0 ]; // uvs based on frame rect // uvs const lu = rect.x / texWidth; const bv = 1.0 - rect.y / texHeight; const ru = (rect.x + rect.z) / texWidth; const tv = 1.0 - (rect.y + rect.w) / texHeight; const uvs = [ lu, bv, ru, bv, ru, tv, lu, tv ]; const geom = new Geometry(); geom.positions = positions; geom.normals = spriteNormals; geom.uvs = uvs; geom.indices = spriteIndices; return Mesh.fromGeometry(this._device, geom); } _create9SliceMesh() { // Check the supplied options and provide defaults for unspecified ones const he = Vec2.ONE; const ws = 3; const ls = 3; // Variable declarations const positions = []; const normals = []; const uvs = []; const indices = []; // Generate plane as follows (assigned UVs denoted at corners): // (0,1)x---------x(1,1) // | | // | | // | O--X |length // | | | // | Z | // (0,0)x---------x(1,0) // width let vcounter = 0; for(let i = 0; i <= ws; i++){ const u = i === 0 || i === ws ? 0 : 1; for(let j = 0; j <= ls; j++){ const x = -he.x + 2.0 * he.x * (i <= 1 ? 0 : 3) / ws; const y = 0.0; const z = -(-he.y + 2.0 * he.y * (j <= 1 ? 0 : 3) / ls); const v = j === 0 || j === ls ? 0 : 1; positions.push(-x, y, z); normals.push(0.0, 1.0, 0.0); uvs.push(u, v); if (i < ws && j < ls) { indices.push(vcounter + ls + 1, vcounter + 1, vcounter); indices.push(vcounter + ls + 1, vcounter + ls + 2, vcounter + 1); } vcounter++; } } const geom = new Geometry(); geom.positions = positions; geom.normals = normals; geom.uvs = uvs; geom.indices = indices; return Mesh.fromGeometry(this._device, geom); } _onSetFrames(frames) { if (this._updatingProperties) { this._meshesDirty = true; } else { this._createMeshes(); } } _onFrameChanged(frameKey, frame) { const idx = this._frameKeys.indexOf(frameKey); if (idx < 0) return; if (frame) { // only re-create frame for simple render mode, since // 9-sliced meshes don't need frame info to create their mesh if (this.renderMode === SPRITE_RENDERMODE_SIMPLE) { this._meshes[idx] = this._createSimpleMesh(frame); } } else { this._meshes[idx] = null; } this.fire('set:meshes'); } _onFrameRemoved(frameKey) { const idx = this._frameKeys.indexOf(frameKey); if (idx < 0) return; this._meshes[idx] = null; this.fire('set:meshes'); } startUpdate() { this._updatingProperties = true; this._meshesDirty = false; } endUpdate() { this._updatingProperties = false; if (this._meshesDirty && this._atlas && this._frameKeys) { this._createMeshes(); } this._meshesDirty = false; } /** * Free up the meshes created by the sprite. */ destroy() { for (const mesh of this._meshes){ if (mesh) { mesh.destroy(); } } this._meshes.length = 0; } } export { Sprite };