diff --git a/src/room/floorplan/pixi-tilemap/CanvasTileRenderer.ts b/src/room/floorplan/pixi-tilemap/CanvasTileRenderer.ts new file mode 100644 index 00000000..031940a5 --- /dev/null +++ b/src/room/floorplan/pixi-tilemap/CanvasTileRenderer.ts @@ -0,0 +1,21 @@ +import * as PIXI from 'pixi.js'; +window.PIXI = PIXI; + +export class CanvasTileRenderer +{ + renderer: PIXI.Renderer; + tileAnim = [0, 0]; + dontUseTransform = false; + + constructor(renderer: PIXI.Renderer) + { + this.renderer = renderer; + this.tileAnim = [0, 0]; + } +} +const cr = (window.PIXI as any).CanvasRenderer; +if(cr) +{ + console.warn('REGISTER'); + cr.registerPlugin('tilemap', CanvasTileRenderer); +} diff --git a/src/room/floorplan/pixi-tilemap/CompositeRectTileLayer.ts b/src/room/floorplan/pixi-tilemap/CompositeRectTileLayer.ts new file mode 100644 index 00000000..163741c6 --- /dev/null +++ b/src/room/floorplan/pixi-tilemap/CompositeRectTileLayer.ts @@ -0,0 +1,286 @@ +import { Container, Matrix, Renderer, Texture } from 'pixi.js'; +import { Constant } from './Constant'; +import { RectTileLayer } from './RectTileLayer'; + +export class CompositeRectTileLayer extends Container +{ + constructor(zIndex?: number, bitmaps?: Array, texPerChild?: number, alpha?: number) + { + super(); + // eslint-disable-next-line prefer-spread,prefer-rest-params + this.initialize.apply(this, arguments); + } + + z: number; + zIndex: number; + modificationMarker = 0; + shadowColor = new Float32Array([0.0, 0.0, 0.0, 0.5]); + _globalMat: Matrix = null; + _lastLayer: RectTileLayer = null; + + texPerChild: number; + + initialize(zIndex?: number, bitmaps?: Array, texPerChild?: number, alpha?: number) + { + if(texPerChild as any === true) + { + //old format, ignore it! + texPerChild = 0; + } + this.z = this.zIndex = zIndex; + this.alpha = alpha ?? 1.0; + this.texPerChild = texPerChild || Constant.boundCountPerBuffer * Constant.maxTextures; + if(bitmaps) + { + this.setBitmaps(bitmaps); + } + } + + setBitmaps(bitmaps: Array) + { + for(let i=0;i 5.2.1, it does not exist there.'); + } + } + const texPerChild = this.texPerChild; + const len1 = this.children.length; + const len2 = Math.ceil(bitmaps.length / texPerChild); + let i: number; + for(i = 0; i < len1; i++) + { + (this.children[i] as RectTileLayer).textures = bitmaps.slice(i * texPerChild, (i + 1) * texPerChild); + } + for(i = len1; i < len2; i++) + { + const layer = new RectTileLayer(this.zIndex, bitmaps.slice(i * texPerChild, (i + 1) * texPerChild)); + layer.compositeParent = true; + layer.offsetX = Constant.boundSize; + layer.offsetY = Constant.boundSize; + this.addChild(layer); + } + } + + clear() + { + for(let i = 0; i < this.children.length; i++) + { + (this.children[i] as RectTileLayer).clear(); + } + this.modificationMarker = 0; + } + + addRect(textureIndex: number, u: number, v: number, x: number, y: number, tileWidth: number, tileHeight: number, animX?: number, animY?: number, rotate?: number, animWidth?: number, animHeight?: number, alpha?: number): this + { + const childIndex: number = textureIndex / this.texPerChild >> 0; + const textureId: number = textureIndex % this.texPerChild; + + if(this.children[childIndex] && (this.children[childIndex] as RectTileLayer).textures) + { + this._lastLayer = (this.children[childIndex] as RectTileLayer); + const tileAlpha = this.worldAlpha * (alpha ?? 1.0); + this._lastLayer.addRect(textureId, u, v, x, y, tileWidth, tileHeight, animX, animY, rotate, animWidth, animHeight, tileAlpha); + } + else + { + this._lastLayer = null; + } + + return this; + } + + tileRotate(rotate: number): this + { + if(this._lastLayer) + { + this._lastLayer.tileRotate(rotate); + } + return this; + } + + tileAnimX(offset: number, count: number): this + { + if(this._lastLayer) + { + this._lastLayer.tileAnimX(offset, count); + } + return this; + } + + tileAnimY(offset: number, count: number): this + { + if(this._lastLayer) + { + this._lastLayer.tileAnimY(offset, count); + } + return this; + } + + addFrame(texture_: Texture | string | number, x: number, y: number, animX?: number, animY?: number, animWidth?: number, animHeight?: number, alpha?: number, yaxis?: number, xaxis?: number): this + { + let texture: Texture; + let layer: RectTileLayer = null; + let ind = 0; + const children = this.children; + + this._lastLayer = null; + if(typeof texture_ === 'number') + { + const childIndex = texture_ / this.texPerChild >> 0; + layer = children[childIndex] as RectTileLayer; + + if(!layer) + { + layer = children[0] as RectTileLayer; + if(!layer) + { + return this; + } + ind = 0; + } + else + { + ind = texture_ % this.texPerChild; + } + + texture = layer.textures[ind]; + } + else + { + if(typeof texture_ === 'string') + { + texture = Texture.from(texture_); + } + else + { + texture = texture_ as Texture; + } + + for(let i = 0; i < children.length; i++) + { + const child = children[i] as RectTileLayer; + const tex = child.textures; + for(let j = 0; j < tex.length; j++) + { + if(tex[j].baseTexture === texture.baseTexture) + { + layer = child; + ind = j; + break; + } + } + if(layer) + { + break; + } + } + + if(!layer) + { + for(let i = 0; i < children.length; i++) + { + const child = children[i] as RectTileLayer; + if(child.textures.length < this.texPerChild) + { + layer = child; + ind = child.textures.length; + child.textures.push(texture); + break; + } + } + if(!layer) + { + layer = new RectTileLayer(this.zIndex, texture); + layer.compositeParent = true; + layer.offsetX = Constant.boundSize; + layer.offsetY = Constant.boundSize; + this.addChild(layer); + ind = 0; + } + } + } + + this._lastLayer = layer; + const tileAlpha = this.worldAlpha * (alpha?? 1.0); + layer.addRect(ind, texture.frame.x, texture.frame.y, x, y, texture.orig.width, texture.orig.height, animX, animY, texture.rotate, animWidth, animHeight, tileAlpha, yaxis, xaxis); + return this; + } + + renderCanvas(renderer: any) + { + if(!this.visible || this.worldAlpha <= 0 || !this.renderable) + { + return; + } + const plugin = renderer.plugins.tilemap; + if(!plugin.dontUseTransform) + { + const wt = this.worldTransform; + renderer.context.setTransform( + wt.a, + wt.b, + wt.c, + wt.d, + wt.tx * renderer.resolution, + wt.ty * renderer.resolution + ); + } + const layers = this.children; + for(let i = 0; i < layers.length; i++) + { + (layers[i] as RectTileLayer).renderCanvasCore(renderer); + } + } + + render(renderer: Renderer) + { + if(!this.visible || this.worldAlpha <= 0 || !this.renderable) + { + return; + } + const plugin = (renderer.plugins as any)['tilemap']; + const shader = plugin.getShader(); + renderer.batch.setObjectRenderer(plugin); + //TODO: dont create new array, please + this._globalMat = shader.uniforms.projTransMatrix; + renderer.globalUniforms.uniforms.projectionMatrix.copyTo(this._globalMat).append(this.worldTransform); + shader.uniforms.shadowColor = this.shadowColor; + shader.uniforms.animationFrame = plugin.tileAnim; + renderer.shader.bind(shader, false); + const layers = this.children; + for(let i = 0; i < layers.length; i++) + { + (layers[i] as RectTileLayer).renderWebGLCore(renderer, plugin); + } + } + + isModified(anim: boolean) + { + const layers = this.children; + if(this.modificationMarker !== layers.length) + { + return true; + } + for(let i = 0; i < layers.length; i++) + { + if((layers[i] as RectTileLayer).isModified(anim)) + { + return true; + } + } + return false; + } + + clearModify() + { + const layers = this.children; + this.modificationMarker = layers.length; + for(let i = 0; i < layers.length; i++) + { + (layers[i] as RectTileLayer).clearModify(); + } + } +} diff --git a/src/room/floorplan/pixi-tilemap/Constant.ts b/src/room/floorplan/pixi-tilemap/Constant.ts new file mode 100644 index 00000000..a4d3f799 --- /dev/null +++ b/src/room/floorplan/pixi-tilemap/Constant.ts @@ -0,0 +1,11 @@ +import { SCALE_MODES } from 'pixi.js'; + +export const Constant = { + maxTextures: 16, + bufferSize: 2048, + boundSize: 1024, + boundCountPerBuffer: 1, + use32bitIndex: false, + SCALE_MODE: SCALE_MODES.LINEAR, + DO_CLEAR: true +}; diff --git a/src/room/floorplan/pixi-tilemap/MultiTextureResource.ts b/src/room/floorplan/pixi-tilemap/MultiTextureResource.ts new file mode 100644 index 00000000..19e26afb --- /dev/null +++ b/src/room/floorplan/pixi-tilemap/MultiTextureResource.ts @@ -0,0 +1,61 @@ +import { resources } from '@pixi/core'; +import { BaseTexture, Sprite, Texture } from 'pixi.js'; + +export interface IMultiTextureOptions { + boundCountPerBuffer: number; + boundSize: number; + bufferSize: number; + DO_CLEAR?: boolean; +} + +export class MultiTextureResource extends resources.Resource +{ + constructor(options: IMultiTextureOptions) + { + super(options.bufferSize, options.bufferSize); + + const bounds = this.boundSprites; + const dirties = this.dirties; + this.boundSize = options.boundSize; + for(let j = 0; j < options.boundCountPerBuffer; j++) + { + const spr = new Sprite(); + spr.position.x = options.boundSize * (j & 1); + spr.position.y = options.boundSize * (j >> 1); + bounds.push(spr); + dirties.push(0); + } + this.DO_CLEAR = !!options.DO_CLEAR; + } + + DO_CLEAR = false; + boundSize: number = 0; + _clearBuffer: Uint8Array = null; + + bind(baseTexture: BaseTexture) + { + if(this.baseTex) + { + throw new Error('Only one baseTexture is allowed for this resource!'); + } + this.baseTex = baseTexture; + super.bind(baseTexture); + } + + baseTex: BaseTexture = null; + boundSprites: Array = []; + dirties: Array = []; + + setTexture(ind: number, texture: Texture) + { + const spr = this.boundSprites[ind]; + if(spr.texture.baseTexture === texture.baseTexture) + { + return; + } + spr.texture = texture; + this.baseTex.update(); + this.dirties[ind] = (this.baseTex as any).dirtyId; + } + +} diff --git a/src/room/floorplan/pixi-tilemap/RectTileLayer.ts b/src/room/floorplan/pixi-tilemap/RectTileLayer.ts new file mode 100644 index 00000000..ad6e2bda --- /dev/null +++ b/src/room/floorplan/pixi-tilemap/RectTileLayer.ts @@ -0,0 +1,491 @@ +import { Container, DRAW_MODES, groupD8, Matrix, Renderer, Texture } from 'pixi.js'; +import { Constant } from './Constant'; +import { RectTileGeom } from './RectTileShader'; +import { TileRenderer } from './TileRenderer'; + +enum PointStruct { + U = 0, + V, + X, + Y, + TileWidth, + TileHeight, + Rotate, + AnimX, + AnimY, + TextureIndex, + AnimCountX, + AnimCountY, + Alpha, + YAxis, + XAxis +} +export const POINT_STRUCT_SIZE_TWO = (Object.keys(PointStruct).length / 2); +//export const POINT_STRUCT_SIZE = 12; +export class RectTileLayer extends Container +{ + constructor(zIndex: number, texture: Texture | Array) + { + super(); + this.initialize(zIndex, texture); + } + + zIndex = 0; + modificationMarker = 0; + _$_localBounds = new PIXI.Bounds(); + shadowColor = new Float32Array([0.0, 0.0, 0.0, 0.5]); + _globalMat: Matrix = null; + + pointsBuf: Array = []; + hasAnim = false; + textures: Array; + + offsetX = 0; + offsetY = 0; + compositeParent = false; + + initialize(zIndex: number, textures: Texture | Array) + { + if(!textures) + { + textures = []; + } + else if(!(textures instanceof Array) && (textures as Texture).baseTexture) + { + textures = [textures as Texture]; + } + this.textures = textures as Array; + this.zIndex = zIndex; + // this.visible = false; + } + + clear() + { + this.pointsBuf.length = 0; + this.modificationMarker = 0; + this._$_localBounds.clear(); + this.hasAnim = false; + } + + addFrame(texture_: Texture | string | number, x: number, y: number, animX: number, animY: number) + { + let texture: Texture; + let textureIndex = 0; + + if(typeof texture_ === 'number') + { + textureIndex = texture_; + texture = this.textures[textureIndex]; + } + else + { + if(typeof texture_ === 'string') + { + texture = Texture.from(texture_); + } + else + { + texture = texture_ as Texture; + } + + let found = false; + const textureList = this.textures; + for(let i = 0; i < textureList.length; i++) + { + if(textureList[i].baseTexture === texture.baseTexture) + { + textureIndex = i; + found = true; + break; + } + } + + if(!found) + { + // textureIndex = this.textures.length; + // this.textures.push(texture); + return false; + } + } + + this.addRect(textureIndex, texture.frame.x, texture.frame.y, x, y, texture.orig.width, texture.orig.height, animX, animY, texture.rotate); + return true; + } + + addRect(textureIndex: number, u: number, v: number, x: number, y: number, tileWidth: number, tileHeight: number, + animX: number = 0, animY: number = 0, rotate: number = 0, animCountX: number = 1024, animCountY: number = 1024, alpha: number = 1, yaxis?: number, xaxis?: number): this + { + const pb = this.pointsBuf; + this.hasAnim = this.hasAnim || animX > 0 || animY > 0; + pb.push(u); + pb.push(v); + pb.push(x); + pb.push(y); + pb.push(tileWidth); + pb.push(tileHeight); + pb.push(rotate); + pb.push(animX | 0); + pb.push(animY | 0); + pb.push(textureIndex); + pb.push(animCountX); + pb.push(animCountY); + pb.push(alpha); + pb.push(yaxis); + pb.push(xaxis); + + this._$_localBounds.addFramePad(x, y, x+tileWidth, y+tileHeight, 0, 0); + + return this; + } + + tileRotate(rotate: number) + { + const pb = this.pointsBuf; + pb[pb.length - (POINT_STRUCT_SIZE_TWO - PointStruct.TextureIndex)] = rotate; + } + + tileAnimX(offset: number, count: number) + { + const pb = this.pointsBuf; + + pb[pb.length - (POINT_STRUCT_SIZE_TWO - PointStruct.AnimX)] = offset; + pb[pb.length - (POINT_STRUCT_SIZE_TWO - PointStruct.AnimCountX)] = count; + } + + tileAnimY(offset: number, count: number) + { + const pb = this.pointsBuf; + + pb[pb.length - (POINT_STRUCT_SIZE_TWO - PointStruct.AnimY)] = offset; + pb[pb.length - (POINT_STRUCT_SIZE_TWO - PointStruct.AnimCountY)] = count; + } + + tileAlpha(alpha: number) + { + const pb = this.pointsBuf; + pb[pb.length - (POINT_STRUCT_SIZE_TWO - PointStruct.Alpha)] = alpha; + } + + renderCanvas(renderer: any) + { + const plugin = renderer.plugins.tilemap; + if(!plugin.dontUseTransform) + { + const wt = this.worldTransform; + renderer.context.setTransform( + wt.a, + wt.b, + wt.c, + wt.d, + wt.tx * renderer.resolution, + wt.ty * renderer.resolution + ); + } + this.renderCanvasCore(renderer); + } + + renderCanvasCore(renderer: any) + { + if(this.textures.length === 0) return; + const points = this.pointsBuf; + renderer.context.fillStyle = '#000000'; + for(let i = 0, n = points.length; i < n; i += POINT_STRUCT_SIZE_TWO) + { + let x1 = points[i + PointStruct.U], y1 = points[i + PointStruct.V]; + const x2 = points[i + PointStruct.X], y2 = points[i + PointStruct.Y]; + const w = points[i + PointStruct.TileWidth]; + const h = points[i + PointStruct.TileHeight]; + const rotate = points[i + PointStruct.Rotate]; + x1 += points[i + PointStruct.AnimX] * renderer.plugins.tilemap.tileAnim[0]; + y1 += points[i + PointStruct.AnimY] * renderer.plugins.tilemap.tileAnim[1]; + const textureIndex = points[i + PointStruct.TextureIndex]; + const alpha = points[i + PointStruct.Alpha]; + // canvas does not work with rotate yet + if(textureIndex >= 0) + { + renderer.context.globalAlpha = alpha; + renderer.context.drawImage((this.textures[textureIndex].baseTexture as any).getDrawableSource(), x1, y1, w, h, x2, y2, w, h); + } + else + { + renderer.context.globalAlpha = 0.5; + renderer.context.fillRect(x2, y2, w, h); + renderer.context.globalAlpha = 1; + } + renderer.context.globalAlpha = 1; + } + } + + vbId = 0; + vb: RectTileGeom = null; + vbBuffer: ArrayBuffer = null; + vbArray: Float32Array = null; + vbInts: Uint32Array = null; + + destroyVb() + { + if(this.vb) + { + this.vb.destroy(); + this.vb = null; + } + } + + render(renderer: Renderer) + { + const plugin = (renderer.plugins as any)['tilemap']; + const shader = plugin.getShader(); + renderer.batch.setObjectRenderer(plugin); + this._globalMat = shader.uniforms.projTransMatrix; + renderer.globalUniforms.uniforms.projectionMatrix.copyTo(this._globalMat).append(this.worldTransform); + shader.uniforms.shadowColor = this.shadowColor; + shader.uniforms.animationFrame = plugin.tileAnim; + this.renderWebGLCore(renderer, plugin); + } + + renderWebGLCore(renderer: Renderer, plugin: TileRenderer) + { + const points = this.pointsBuf; + if(points.length === 0) return; + const rectsCount = points.length / POINT_STRUCT_SIZE_TWO; + + const shader = plugin.getShader(); + const textures = this.textures; + if(textures.length === 0) return; + + plugin.bindTextures(renderer, shader, textures); + renderer.shader.bind(shader, false); + + //lost context! recover! + let vb = this.vb; + if(!vb) + { + vb = plugin.createVb(); + this.vb = vb; + this.vbId = (vb as any).id; + this.vbBuffer = null; + this.modificationMarker = 0; + } + + plugin.checkIndexBuffer(rectsCount, vb); + const boundCountPerBuffer = Constant.boundCountPerBuffer; + + const vertexBuf = vb.getBuffer('aVertexPosition'); + //if layer was changed, re-upload vertices + const vertices = rectsCount * vb.vertPerQuad; + if(vertices === 0) return; + if(this.modificationMarker !== vertices) + { + this.modificationMarker = vertices; + const vs = vb.stride * vertices; + if(!this.vbBuffer || this.vbBuffer.byteLength < vs) + { + //!@#$ happens, need resize + let bk = vb.stride; + while(bk < vs) + { + bk *= 2; + } + this.vbBuffer = new ArrayBuffer(bk); + this.vbArray = new Float32Array(this.vbBuffer); + this.vbInts = new Uint32Array(this.vbBuffer); + vertexBuf.update(this.vbBuffer); + } + + const arr = this.vbArray, ints = this.vbInts; + //upload vertices! + let sz = 0; + //let tint = 0xffffffff; + let textureId = 0; + let shiftU: number = this.offsetX; + let shiftV: number = this.offsetY; + + //let tint = 0xffffffff; + const tint = -1; + + + for(let i = 0; i < points.length; i += POINT_STRUCT_SIZE_TWO) + { + const eps = 0.5; + if(this.compositeParent) + { + const textureIndex = points[i + PointStruct.TextureIndex]; + if(boundCountPerBuffer > 1) + { + //TODO: what if its more than 4? + textureId = (textureIndex >> 2); + shiftU = this.offsetX * (textureIndex & 1); + shiftV = this.offsetY * ((textureIndex >> 1) & 1); + } + else + { + textureId = textureIndex; + shiftU = 0; + shiftV = 0; + } + } + const x = points[i + PointStruct.X], y = points[i + PointStruct.Y]; + const w = points[i + PointStruct.TileWidth], h = points[i + PointStruct.TileHeight]; + const u = points[i + PointStruct.U] + shiftU, v = points[i + PointStruct.V] + shiftV; + let rotate = points[i + PointStruct.Rotate]; + + const animX = points[i + PointStruct.AnimX], animY = points[i + PointStruct.AnimY]; + const animWidth = points[i + PointStruct.AnimCountX] || 1024, animHeight = points[i + PointStruct.AnimCountY] || 1024; + const animXEncoded = animX + (animWidth * 2048); + const animYEncoded = animY + (animHeight * 2048); + const alpha = points[i + PointStruct.Alpha]; + + let u0: number, v0: number, u1: number, v1: number, u2: number, v2: number, u3: number, v3: number; + if(rotate === 0) + { + u0 = u; + v0 = v; + u1 = u + w; + v1 = v; + u2 = u + w; + v2 = v + h; + u3 = u; + v3 = v + h; + } + else + { + let w2 = w / 2; + let h2 = h / 2; + if(rotate % 4 !== 0) + { + w2 = h / 2; + h2 = w / 2; + } + const cX = u + w2; + const cY = v + h2; + + rotate = groupD8.add(rotate, groupD8.NW); + u0 = cX + (w2 * groupD8.uX(rotate)); + v0 = cY + (h2 * groupD8.uY(rotate)); + + rotate = groupD8.add(rotate, 2); // rotate 90 degrees clockwise + u1 = cX + (w2 * groupD8.uX(rotate)); + v1 = cY + (h2 * groupD8.uY(rotate)); + + rotate = groupD8.add(rotate, 2); + u2 = cX + (w2 * groupD8.uX(rotate)); + v2 = cY + (h2 * groupD8.uY(rotate)); + + rotate = groupD8.add(rotate, 2); + u3 = cX + (w2 * groupD8.uX(rotate)); + v3 = cY + (h2 * groupD8.uY(rotate)); + } + + const test= true; + arr[sz++] = x; + arr[sz++] = y; + arr[sz++] = u0; + arr[sz++] = v0; + arr[sz++] = u + eps; + arr[sz++] = v + eps; + arr[sz++] = u + w - eps; + arr[sz++] = v + h - eps; + arr[sz++] = animXEncoded; + arr[sz++] = animYEncoded; + arr[sz++] = textureId; + + if(test) + { + arr[sz++] = alpha; + } + arr[sz++] = x + w; + arr[sz++] = y; + arr[sz++] = u1; + arr[sz++] = v1; + arr[sz++] = u + eps; + arr[sz++] = v + eps; + arr[sz++] = u + w - eps; + arr[sz++] = v + h - eps; + arr[sz++] = animXEncoded; + arr[sz++] = animYEncoded; + arr[sz++] = textureId; + if(test) + { + arr[sz++] = alpha; + } + + + arr[sz++] = x + w; + arr[sz++] = y + h; + arr[sz++] = u2; + arr[sz++] = v2; + arr[sz++] = u + eps; + arr[sz++] = v + eps; + arr[sz++] = u + w - eps; + arr[sz++] = v + h - eps; + arr[sz++] = animXEncoded; + arr[sz++] = animYEncoded; + arr[sz++] = textureId; + if(test) + { + arr[sz++] = alpha; + } + + arr[sz++] = x; + arr[sz++] = y + h; + arr[sz++] = u3; + arr[sz++] = v3; + arr[sz++] = u + eps; + arr[sz++] = v + eps; + arr[sz++] = u + w - eps; + arr[sz++] = v + h - eps; + arr[sz++] = animXEncoded; + arr[sz++] = animYEncoded; + arr[sz++] = textureId; + if(test) + { + arr[sz++] = alpha; + } + } + + vertexBuf.update(arr); + } + + (renderer.geometry as any).bind(vb, shader); + renderer.geometry.draw(DRAW_MODES.TRIANGLES, rectsCount * 6, 0); + } + + isModified(anim: boolean) + { + if(this.modificationMarker !== this.pointsBuf.length || + anim && this.hasAnim) + { + return true; + } + return false; + } + + clearModify() + { + this.modificationMarker = this.pointsBuf.length; + } + + protected _calculateBounds(): void + { + const { minX, minY, maxX, maxY } = this._$_localBounds; + + this._bounds.addFrame(this.transform, minX, minY, maxX, maxY); + } + + public getLocalBounds(rect?: PIXI.Rectangle): PIXI.Rectangle + { + // we can do a fast local bounds if the sprite has no children! + if(this.children.length === 0) + { + return this._$_localBounds.getRectangle(rect); + } + + return super.getLocalBounds.call(this, rect); + } + + destroy(options?: any) + { + super.destroy(options); + this.destroyVb(); + } +} diff --git a/src/room/floorplan/pixi-tilemap/RectTileShader.ts b/src/room/floorplan/pixi-tilemap/RectTileShader.ts new file mode 100644 index 00000000..8dcf5123 --- /dev/null +++ b/src/room/floorplan/pixi-tilemap/RectTileShader.ts @@ -0,0 +1,110 @@ +import { Buffer, Geometry, Matrix, Shader } from 'pixi.js'; +import * as shaderGenerator from './shaderGenerator'; + +const rectShaderFrag = ` +varying vec2 vTextureCoord; +varying vec4 vFrame; +varying float vTextureId; +varying float vAlpha; +uniform vec4 shadowColor; +uniform sampler2D uSamplers[%count%]; +uniform vec2 uSamplerSize[%count%]; + +void main(void){ + + + vec2 textureCoord = clamp(vTextureCoord, vFrame.xy, vFrame.zw); + float textureId = floor(vTextureId + 0.5); + + vec4 color; + vec3 tint = vec3(1, 0.8, 0.5); // reddish + %forloop% + gl_FragColor = (color * vAlpha) + (gl_FragColor * (1.0 - vAlpha)); +} +`; +const rectShaderVert = ` +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aFrame; +attribute vec2 aAnim; +attribute float aTextureId; +attribute float aAlpha; + +uniform mat3 projTransMatrix; +uniform vec2 animationFrame; + +varying vec2 vTextureCoord; +varying float vTextureId; +varying vec4 vFrame; +varying float vAlpha; + +void main(void){ + gl_Position = vec4((projTransMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + vec2 animCount = floor((aAnim + 0.5) / 2048.0); + vec2 animFrameOffset = aAnim - animCount * 2048.0; + vec2 animOffset = animFrameOffset * floor(mod(animationFrame + 0.5, animCount)); + + vTextureCoord = aTextureCoord + animOffset; + vFrame = aFrame + vec4(animOffset, animOffset); + vTextureId = aTextureId; + vAlpha = aAlpha; +} +`; + +export abstract class TilemapShader extends Shader +{ + maxTextures = 0; + + constructor(maxTextures: number, shaderVert: string, shaderFrag: string) + { + super( + new PIXI.Program( + shaderVert, + shaderFrag), + { + animationFrame: new Float32Array(2), + uSamplers: [], + uSamplerSize: [], + projTransMatrix: new Matrix() + } + ); + this.maxTextures = maxTextures; + shaderGenerator.fillSamplers(this, this.maxTextures); + } +} + +export class RectTileShader extends TilemapShader +{ + constructor(maxTextures: number) + { + super( + maxTextures, + rectShaderVert, + shaderGenerator.generateFragmentSrc(maxTextures, rectShaderFrag) + ); + shaderGenerator.fillSamplers(this, this.maxTextures); + } +} + +export class RectTileGeom extends Geometry +{ + vertSize = 12; + vertPerQuad = 4; + stride = this.vertSize * 4; + lastTimeAccess = 0; + + constructor() + { + super(); + const buf = this.buf = new Buffer(new Float32Array(2), true, false); + this.addAttribute('aVertexPosition', buf, 0, false, 0, this.stride, 0) + .addAttribute('aTextureCoord', buf, 0, false, 0, this.stride, 2 * 4) + .addAttribute('aFrame', buf, 0, false, 0, this.stride, 4 * 4) + .addAttribute('aAnim', buf, 0, false, 0, this.stride, 8 * 4) + .addAttribute('aTextureId', buf, 0, false, 0, this.stride, 10 * 4) + .addAttribute('aTextureId', buf, 0, false, 0, this.stride, 10 * 4) + .addAttribute('aAlpha', buf, 0, false, 0, this.stride, 11 * 4); + } + + buf: PIXI.Buffer; +} diff --git a/src/room/floorplan/pixi-tilemap/TileRenderer.ts b/src/room/floorplan/pixi-tilemap/TileRenderer.ts new file mode 100644 index 00000000..cb961e3e --- /dev/null +++ b/src/room/floorplan/pixi-tilemap/TileRenderer.ts @@ -0,0 +1,165 @@ +import { BaseTexture, Buffer, ObjectRenderer, Renderer, Texture, WRAP_MODES } from 'pixi.js'; +import { Constant } from './Constant'; +import { MultiTextureResource } from './MultiTextureResource'; +import type { TilemapShader } from './RectTileShader'; +import { RectTileGeom, RectTileShader } from './RectTileShader'; + +/** + * Renderer for rectangle tiles. + */ +export class TileRenderer extends ObjectRenderer +{ + renderer: Renderer; + gl: WebGLRenderingContext; + sn: number = -1; + indexBuffer: PIXI.Buffer = null; + ibLen: number = 0; + tileAnim = [0, 0]; + texLoc: Array = []; + + rectShader: RectTileShader; + texResources: Array = []; + + constructor(renderer: Renderer) + { + super(renderer); + this.rectShader = new RectTileShader(Constant.maxTextures); + this.indexBuffer = new Buffer(undefined, true, true); + this.checkIndexBuffer(2000); + this.initBounds(); + } + + initBounds() + { + if(Constant.boundCountPerBuffer <= 1) + { + return; + } + + const maxTextures = Constant.maxTextures; + for(let i = 0; i < maxTextures; i++) + { + const resource = new MultiTextureResource(Constant); + // @ts-ignore + const baseTex = new BaseTexture(resource); + baseTex.scaleMode = Constant.SCALE_MODE; + baseTex.wrapMode = WRAP_MODES.CLAMP; + this.texResources.push(resource); + } + } + + bindTexturesWithoutRT(renderer: Renderer, shader: TilemapShader, textures: Array) + { + const samplerSize: Array = (shader as any).uniforms.uSamplerSize; + this.texLoc.length = 0; + for(let i = 0; i < textures.length; i++) + { + const texture = textures[i]; + if(!texture || !texture.valid) + { + return; + } + renderer.texture.bind(textures[i], i); + //TODO: add resolution here + samplerSize[i * 2] = 1.0 / textures[i].baseTexture.width; + samplerSize[i * 2 + 1] = 1.0 / textures[i].baseTexture.height; + } + (shader as any).uniforms.uSamplerSize = samplerSize; + } + + bindTextures(renderer: Renderer, shader: TilemapShader, textures: Array) + { + const len = textures.length; + const maxTextures = Constant.maxTextures; + if(len > Constant.boundCountPerBuffer * maxTextures) + { + return; + } + if(Constant.boundCountPerBuffer <= 1) + { + this.bindTexturesWithoutRT(renderer, shader, textures); + return; + } + + let i = 0; + for(; i < len; i++) + { + const texture = textures[i]; + if(!texture || !texture.valid) continue; + const multi = this.texResources[i >> 2]; + multi.setTexture(i & 3, texture); + } + + const gltsUsed = (i + 3) >> 2; + for(i = 0; i < gltsUsed; i++) + { + //remove "i, true" after resolving a bug + renderer.texture.bind(this.texResources[i].baseTex, i); + } + } + + start() + { + //sorry, nothing + } + + createVb() + { + const geom = new RectTileGeom(); + geom.addIndex(this.indexBuffer); + geom.lastTimeAccess = Date.now(); + return geom; + } + + checkIndexBuffer(size: number, vb: RectTileGeom = null) + { + const totalIndices = size * 6; + + if(totalIndices <= this.ibLen) + { + return; + } + + let len = totalIndices; + while(len < totalIndices) + { + len <<= 1; + } + + this.ibLen = totalIndices; + this.indexBuffer.update((PIXI as any).utils.createIndicesForQuads(size, + Constant.use32bitIndex ? new Uint32Array(size * 6) : undefined)); + + // TODO: create new index buffer instead? + // if (vb) { + // const curIndex = vb.getIndex(); + // if (curIndex !== this.indexBuffer && (curIndex.data as any).length < totalIndices) { + // this.swapIndex(vb, this.indexBuffer); + // } + // } + } + + // swapIndex(geom: PIXI.Geometry, indexBuf: PIXI.Buffer) { + // let buffers = (geom as any).buffers; + // const oldIndex = geom.getIndex(); + // let ind = buffers.indexOf(oldIndex); + // if (ind >= 0) { + // buffers.splice(ind, 1); + // } + // geom.addIndex(indexBuf); + // } + + getShader(): TilemapShader + { + return this.rectShader; + } + + destroy() + { + super.destroy(); + // this.rectShader.destroy(); + this.rectShader = null; + } +} + +Renderer.registerPlugin('tilemap', TileRenderer as any); diff --git a/src/room/floorplan/pixi-tilemap/index.ts b/src/room/floorplan/pixi-tilemap/index.ts new file mode 100644 index 00000000..38e72430 --- /dev/null +++ b/src/room/floorplan/pixi-tilemap/index.ts @@ -0,0 +1,8 @@ +export * from './CanvasTileRenderer'; +export * from './CompositeRectTileLayer'; +export * from './Constant'; +export * from './MultiTextureResource'; +export * from './RectTileLayer'; +export * from './RectTileShader'; +export * from './shaderGenerator'; +export * from './TileRenderer'; diff --git a/src/room/floorplan/pixi-tilemap/shaderGenerator.ts b/src/room/floorplan/pixi-tilemap/shaderGenerator.ts new file mode 100644 index 00000000..c7fe15ef --- /dev/null +++ b/src/room/floorplan/pixi-tilemap/shaderGenerator.ts @@ -0,0 +1,57 @@ +import { Constant } from './Constant'; +import type { TilemapShader } from './RectTileShader'; + +export function fillSamplers(shader: TilemapShader, maxTextures: number) +{ + const sampleValues: Array = []; + for(let i = 0; i < maxTextures; i++) + { + sampleValues[i] = i; + } + shader.uniforms.uSamplers = sampleValues; + + const samplerSize: Array = []; + for(let i = 0; i < maxTextures; i++) + { + samplerSize.push(1.0 / Constant.bufferSize); + samplerSize.push(1.0 / Constant.bufferSize); + } + shader.uniforms.uSamplerSize = samplerSize; +} + +export function generateFragmentSrc(maxTextures: number, fragmentSrc: string) +{ + return fragmentSrc.replace(/%count%/gi, maxTextures + '') + .replace(/%forloop%/gi, generateSampleSrc(maxTextures)); +} + +export function generateSampleSrc(maxTextures: number) +{ + let src = ''; + + src += '\n'; + src += '\n'; + + src += 'if(vTextureId <= -1.0) {'; + src += '\n\tcolor = shadowColor;'; + src += '\n}'; + + for(let i = 0; i < maxTextures; i++) + { + src += '\nelse '; + + if(i < maxTextures-1) + { + src += 'if(textureId == ' + i + '.0)'; + } + + src += '\n{'; + src += '\n\tcolor = texture2D(uSamplers['+i+'], textureCoord * uSamplerSize['+i+']);'; + src += '\n}'; + } + + src += '\n'; + src += '\n'; + + return src; +}