UNPKG

@awayjs/stage

Version:
378 lines (295 loc) 10.1 kB
import { ByteArray } from '@awayjs/core'; import { AGALTokenizer } from '../aglsl/AGALTokenizer'; import { AGLSLParser } from '../aglsl/AGLSLParser'; import { IProgram } from '../base/IProgram'; import { Settings } from '../Settings'; import { ContextWebGL } from './ContextWebGL'; import { WEBGL_METHOD_MAP } from './WebGLDataMapping'; import { Part } from '../aglsl/assembler/Part'; const TEST_PLACE = /(#define|#version|precision).*\n/gi; export interface IUniformMeta { location: WebGLUniformLocation; type: number; size: number; } export interface IAttrMeta { location: number; type: number; size: number; } export interface INativePropgram { program: WebGLProgram; uniforms: Record<string, IUniformMeta>; attributes: Record<string, IAttrMeta>; usage: number; } export class ProgramWebGL implements IProgram { public static programCache: Record<string, INativePropgram> = {}; private static ProgramID = 0; private static _tokenizer: AGALTokenizer = new AGALTokenizer(); private static _aglslParser: AGLSLParser = new AGLSLParser(); private static _uniformLocationNameDictionary: Array<string> = ['fc', 'fs', 'vc']; public name: string; private _id: number = ProgramWebGL.ProgramID++; private _gl: WebGLRenderingContext; private _program: INativePropgram; private _uniformCache: Array<string> = new Array(16); private _focusId: number = 0; /** * @description Changed only after rebound a shader. Can be used for partial upload */ public get focusId() { return this._focusId; } constructor(private _context: ContextWebGL) { this._gl = _context._gl; } public upload(vertexProgram: ByteArray | Part, fragmentProgram: ByteArray | Part): void { //detect whether highp can be used const vertexPrecision = this._gl.getShaderPrecisionFormat( this._gl.VERTEX_SHADER, this._gl.HIGH_FLOAT).precision; const fragmentPrecision = this._gl.getShaderPrecisionFormat( this._gl.FRAGMENT_SHADER, this._gl.HIGH_FLOAT).precision; const vertexString: string = ProgramWebGL._aglslParser.parse( ProgramWebGL._tokenizer.decribeAGALPart(vertexProgram), vertexPrecision ? 'highp' : 'mediump', this._context.glVersion === 2 ); const fragmentString: string = ProgramWebGL._aglslParser.parse( ProgramWebGL._tokenizer.decribeAGALPart(fragmentProgram), fragmentPrecision ? 'highp' : 'mediump', this._context.glVersion === 2 ); if (!this.name) { this.name = 'PROG_AGAL_' + this._id; } this.uploadRaw(vertexString, fragmentString); } public uploadRaw(vertexGLSL: string, fragmentGLSL: string) { if (!this.name) { this.name = 'PROG_GLSL_' + this._id; } const key = vertexGLSL + fragmentGLSL; if (key in ProgramWebGL.programCache) { this._program = ProgramWebGL.programCache[key]; this._program.usage++; return; } vertexGLSL = this.insertName(vertexGLSL); fragmentGLSL = this.insertName(fragmentGLSL); const vertexShader = this._gl.createShader(this._gl.VERTEX_SHADER); const fragmentShader = this._gl.createShader(this._gl.FRAGMENT_SHADER); const program = this._gl.createProgram(); this._context.stats.counter.program++; this._gl.shaderSource(vertexShader, vertexGLSL); this._gl.compileShader(vertexShader); if (!this._gl.getShaderParameter(vertexShader, this._gl.COMPILE_STATUS)) throw new Error(this._gl.getShaderInfoLog(vertexShader)); this._gl.shaderSource(fragmentShader, fragmentGLSL); this._gl.compileShader(fragmentShader); if (!this._gl.getShaderParameter(fragmentShader, this._gl.COMPILE_STATUS)) throw new Error(this._gl.getShaderInfoLog(fragmentShader)); this._gl.attachShader(program, vertexShader); this._gl.attachShader(program, fragmentShader); this._gl.linkProgram(program); if (!this._gl.getProgramParameter(program, this._gl.LINK_STATUS)) throw new Error(this._gl.getProgramInfoLog(program)); // they will alive while any linked programs alive this._gl.deleteShader(vertexShader); this._gl.deleteShader(fragmentShader); this._program = { program: program, uniforms: {}, attributes: {}, usage: 1 }; ProgramWebGL.programCache[key] = this._program; this.reset(); this.grabLocationData(vertexGLSL, fragmentGLSL); } private grabLocationData(vert: string, frag: string) { const rawUniforms = this._program.uniforms; const rawAttrs = this._program.attributes; const gl = this._gl; const p = this._program.program; const ucount = gl.getProgramParameter(p, gl.ACTIVE_UNIFORMS); const acount = gl.getProgramParameter(p, gl.ACTIVE_ATTRIBUTES); for (let i = 0; i < ucount; i++) { const info = gl.getActiveUniform(p, i); const idx = info.name.indexOf('['); const record = { type: info.type, size: info.size, location: gl.getUniformLocation(p, info.name) }; rawUniforms [ idx === -1 ? info.name : info.name.substring(0, idx) ] = record; } for (let i = 0; i < acount; i++) { const info = gl.getActiveAttrib(p, i); rawAttrs[info.name] = { type: info.type, size: info.size, location: gl.getAttribLocation(p, info.name) }; } // MAP fs{numer} to samplers name by their order in shader! // THIS is important, because chrome mobile can reorder uniforms if (Settings.UNSAFE_USE_AUTOINDEXED_SAMPLER) { let searchIndex = frag.indexOf('uniform sampler', 0); let index = 0; while (searchIndex >= 0) { const end = frag.indexOf(';', searchIndex + 2); const block = frag.substring(searchIndex, end).split(' '); const name = block[block.length - 1]; if (!rawUniforms['fs' + index]) { rawUniforms['fs' + index] = rawUniforms[name]; } searchIndex = frag.indexOf('uniform sampler', end); index++; } } // map atributes by write order (how it used in gl source) if (Settings.UNSAFE_USE_AUTOINDEXED_ATTRIBUTES) { let searchIndex = vert.indexOf('attribute', 0); let index = 0; while (searchIndex >= 0) { const end = vert.indexOf(';', searchIndex + 2); const block = vert.substring(searchIndex, end).split(' '); const name = block[block.length - 1]; if (!rawAttrs['va' + index]) { rawAttrs['va' + index] = rawAttrs[name]; } searchIndex = vert.indexOf('attribute', end); index++; } } } protected insertName(shader: string): string { const mathes = shader.match(TEST_PLACE) || []; const last = mathes[mathes.length - 1]; const corret = last ? shader.lastIndexOf(last) + last.length : 0; return shader.substr(0, corret) + `\n#define SHADER_NAME ${this.name}\n\n` + shader.substr(corret); } protected reset() { this._uniformCache.fill(''); } public getUniformLocation( programType: number, indexOrName: number | string = -1 ): WebGLUniformLocation | null { const isIndex = typeof indexOrName === 'number'; const name = isIndex ? ProgramWebGL._getAGALUniformName(programType, <number>indexOrName) : <string>indexOrName; const info = this._program.uniforms[name]; if (!info) return null; return info.location; } public getAttribLocation(index: number): number { const info = this._program.attributes['va' + index]; if (!info) return -1; return info.location; } private static _getAGALUniformName (type: number, index = -1) { return (index === -1) ? this._uniformLocationNameDictionary[type] : this._uniformLocationNameDictionary[type] + index; } private _needCache(index: number, value: Float32Array | number): string { const cached = this._uniformCache[index]; const hash = value instanceof Float32Array ? new Uint32Array(value.buffer).join('') : '' + value; // already uploaded return (cached && hash === cached) ? void 0 : hash; } public uploadUniform(name: string, data: number | number[] | Float32Array): boolean { const info = this._program.uniforms[name]; if (!info) return false; if (!WEBGL_METHOD_MAP[info.type]) { throw ('[ProgramWebGL] Unsupported uniform type:' + info.type); } const { size, method } = WEBGL_METHOD_MAP[info.type]; if (size === 1) { if (typeof data === 'number' || info.size === 1) { this._gl[method](info.location, typeof data === 'number' ? data : data[0]); } else { this._gl[method + 'v'](info.location, data); } return true; } const arr: ArrayLike<number> = <any> data; if (arr.length !== info.size * size) { throw ( `[ProgramWebGL] Invalid data length for ${name}, expected ${info.size * size}, actual ${arr.length}` ); } if (info.type === this._gl.FLOAT_MAT2 || info.type === this._gl.FLOAT_MAT3 || info.type === this._gl.FLOAT_MAT4 ) { this._gl[method](info.location, false, arr); } else { this._gl[method](info.location, arr); } } public uniform1i(type: number, index: number, value: number) { const location = this.getUniformLocation(type, index); if (!location) { return; } if (Settings.ENABLE_UNIFORM_CACHE) { const hash = this._needCache(type * 4 + index + 1, value); // return undef hash if not require to uppload; if (hash === void 0) { return; } this._uniformCache[type * 4 + index + 1] = hash; } this._gl.uniform1i(location, value); } public uniform4fv(type: number, value: Float32Array) { const location = this.getUniformLocation(type); if (!location) { return; } if (Settings.ENABLE_UNIFORM_CACHE) { const hash = this._needCache(type * 4, value); // return undef hash if not require to uppload; if (hash === void 0) { return; } this._uniformCache[type * 4] = hash; } this._gl.uniform4fv(location, value); } public dispose(): void { // not real delete progs, because maybe will be recreted in nearest future // then progs in prety small, we can store 1000 + without overhead // reupload is more expensive // this._context.stats.progs--; // this._gl.deleteProgram(this._program); this.reset(); } public focusProgram(): void { this._focusId++; this._uniformCache.fill(''); this._gl.useProgram(this._program.program); } public get glProgram(): WebGLProgram { return this._program; } }