|
import { doesExist, mustExist, mustGet } from '@apextoaster/js-utils'; |
|
import * as Phaser from 'phaser'; |
|
|
|
import { CENTER_SPLIT } from '../constants'; |
|
import { Point } from '../entity'; |
|
|
|
export interface EffectSampler { |
|
name: string; |
|
source: string; |
|
} |
|
|
|
export interface EffectUniform { |
|
name: string; |
|
value: number; |
|
} |
|
|
|
export interface PipelineEffect { |
|
name: string; |
|
fragment: string; |
|
samplers: Array<EffectSampler>; |
|
vertex: string; |
|
} |
|
|
|
export interface PipelineLayer { |
|
name: string; |
|
uniforms: Array<EffectUniform>; |
|
} |
|
|
|
export interface PipelineData { |
|
name: string; |
|
effects: Array<PipelineEffect>; |
|
layers: Array<PipelineLayer>; |
|
mipmaps: boolean; |
|
screen: Point; |
|
} |
|
|
|
const REQUIRED_BUFFERS = [ |
|
'scene', |
|
'stage', |
|
]; |
|
|
|
const MAX_SAMPLERS = 8; |
|
|
|
export type RenderObject = Phaser.GameObjects.GameObject & Phaser.GameObjects.Components.Transform; |
|
|
|
class EffectPipeline extends Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline { |
|
protected samplers: Array<{ |
|
name: string; |
|
texture: Phaser.GameObjects.RenderTexture; |
|
}>; |
|
|
|
constructor(game: Phaser.Game, effect: PipelineEffect) { |
|
super({ |
|
fragShader: effect.fragment, |
|
game, |
|
renderer: game.renderer, |
|
vertShader: effect.vertex, |
|
}); |
|
|
|
this.samplers = []; |
|
} |
|
|
|
public onBind() { |
|
super.onBind(); |
|
|
|
const program = this.program; |
|
const renderer = this.renderer; |
|
|
|
const sl = Math.min(MAX_SAMPLERS, this.samplers.length); |
|
for (let i = 0; i < sl; ++i) { |
|
const { name, texture } = this.samplers[i]; |
|
|
|
renderer.setTexture2D(texture.glTexture, i); |
|
renderer.setInt1(program, name, i); |
|
} |
|
|
|
return this; |
|
} |
|
|
|
public cacheTextures(samplers: Array<EffectSampler>, buffers: Map<string, Phaser.GameObjects.RenderTexture>) { |
|
for (const { name, source } of samplers) { |
|
const texture = mustGet(buffers, source); |
|
this.samplers.push({ |
|
name, |
|
texture, |
|
}); |
|
} |
|
} |
|
} |
|
|
|
export class PostProcessor { |
|
protected readonly data: PipelineData; |
|
protected readonly scene: Phaser.Scene; |
|
|
|
protected readonly buffers: Map<string, Phaser.GameObjects.RenderTexture>; |
|
protected readonly effects: Map<string, EffectPipeline>; |
|
|
|
protected running: boolean; |
|
protected fillRect?: Phaser.GameObjects.Rectangle; |
|
|
|
constructor(data: PipelineData, scene: Phaser.Scene) { |
|
this.data = data; |
|
this.scene = scene; |
|
this.running = false; |
|
|
|
this.effects = new Map(); |
|
this.buffers = new Map(); |
|
} |
|
|
|
public get screenBuffer() { |
|
return this.getBuffer('stage'); |
|
} |
|
|
|
public getBuffer(key: string) { |
|
return mustGet(this.buffers, key); |
|
} |
|
|
|
public create() { |
|
this.fillRect = this.scene.add.rectangle(0, 0, this.data.screen.x, this.data.screen.y, 0x00, 1.0); |
|
|
|
for (const buffer of REQUIRED_BUFFERS) { |
|
const rt = this.createBuffer(buffer); |
|
this.buffers.set(buffer, rt); |
|
} |
|
|
|
for (const effect of this.data.effects) { |
|
const game = this.scene.game; |
|
const pipeline = new EffectPipeline(game, effect); |
|
this.effects.set(effect.name, pipeline); |
|
|
|
const renderer = game.renderer as Phaser.Renderer.WebGL.WebGLRenderer; |
|
renderer.addPipeline(effect.name, pipeline); |
|
|
|
const rt = this.createBuffer(effect.name); |
|
rt.setPipeline(effect.name); |
|
this.buffers.set(effect.name, rt); |
|
} |
|
|
|
for (const data of this.data.effects) { |
|
const effect = mustGet(this.effects, data.name); |
|
effect.cacheTextures(data.samplers, this.buffers); |
|
} |
|
|
|
this.running = true; |
|
} |
|
|
|
public createBuffer(name: string) { |
|
const rt = this.scene.make.renderTexture({ |
|
height: this.data.screen.y, |
|
width: this.data.screen.x, |
|
x: 0, |
|
y: 0, |
|
}); |
|
|
|
rt.setName(name); |
|
rt.setScrollFactor(0, 0); |
|
rt.setVisible(false); |
|
|
|
this.scene.textures.addRenderTexture(name, rt); |
|
|
|
return rt; |
|
} |
|
|
|
public update(objects: Array<RenderObject>, layers: Array<PipelineLayer>, camera: Phaser.Cameras.Scene2D.Camera, gl: WebGL2RenderingContext) { |
|
if (!this.running) { |
|
return; |
|
} |
|
|
|
const sceneBuffer = mustGet(this.buffers, 'scene'); |
|
const stageBuffer = mustGet(this.buffers, 'stage'); |
|
|
|
const fillRect = mustExist(this.fillRect); |
|
sceneBuffer.draw(fillRect, fillRect.displayWidth / CENTER_SPLIT, fillRect.displayHeight / CENTER_SPLIT); |
|
for (const obj of objects) { |
|
sceneBuffer.draw(obj, Math.floor(-camera.scrollX + obj.x), Math.floor(-camera.scrollY + obj.y)); |
|
} |
|
|
|
if (this.data.mipmaps) { |
|
this.updateMips(sceneBuffer, gl); |
|
} |
|
|
|
stageBuffer.draw(sceneBuffer); |
|
|
|
for (const layer of layers) { |
|
const buffer = mustGet(this.buffers, layer.name); |
|
const effect = mustGet(this.effects, layer.name); |
|
|
|
if (doesExist(layer.uniforms)) { |
|
for (const uniform of layer.uniforms) { |
|
effect.setFloat1(uniform.name, uniform.value); |
|
} |
|
} |
|
|
|
buffer.draw(stageBuffer); |
|
stageBuffer.draw(buffer); |
|
|
|
if (this.data.mipmaps) { |
|
this.updateMips(stageBuffer, gl); |
|
} |
|
} |
|
} |
|
|
|
public updateMips(buffer: Phaser.GameObjects.RenderTexture, gl: WebGL2RenderingContext) { |
|
const active = gl.getParameter(gl.ACTIVE_TEXTURE); |
|
const levels = Math.ceil(Math.log2(Math.max(buffer.height, buffer.width))); |
|
|
|
gl.activeTexture(gl.TEXTURE0); |
|
gl.bindTexture(gl.TEXTURE_2D, buffer.glTexture); |
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); |
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, levels); |
|
gl.generateMipmap(gl.TEXTURE_2D); |
|
|
|
const err = gl.getError(); |
|
if (err > 0) { |
|
/* eslint-disable-next-line */ |
|
console.warn('mipmap error', err); |
|
} |
|
|
|
gl.activeTexture(active); |
|
} |
|
|
|
public stop() { |
|
this.running = false; |
|
|
|
const fillRect = mustExist(this.fillRect); |
|
this.scene.children.remove(fillRect); |
|
|
|
for (const name of this.buffers.keys()) { |
|
const renderer = this.scene.game.renderer as Phaser.Renderer.WebGL.WebGLRenderer; |
|
renderer.removePipeline(name); |
|
this.scene.textures.remove(name); |
|
} |
|
} |
|
} |