Spaces:
Running
Running
| /* global cv */ | |
| import { Canvas, Image, loadImage, ImageData } from 'skia-canvas'; | |
| // threejs内部使用了OffscreenCanvas | |
| //(globalThis as any).OffscreenCanvas = (globalThis as any).OffscreenCanvas || Canvas; | |
| globalThis.ImageData = ImageData; | |
| import createContext from 'gl'; | |
| import * as SHADER_SOURCE from '../../src/pages/playground/scripts/shaders'; | |
| //const cc = <T>(a: T[][]): T[] => a.flat(1); // This is slower! | |
| const cc = <T>(a: T[][]): T[] => { | |
| const result: T[] = []; | |
| for (const x of a) { | |
| for (const e of x) result.push(e); | |
| } | |
| return result; | |
| }; | |
| type RenderContext = ReturnType<typeof createContext>; | |
| class GLCanvas { | |
| ctx: RenderContext; | |
| _width: number = 256; | |
| _height: number = 192; | |
| resizeBuffer: number[]; | |
| constructor(context: RenderContext) { | |
| this.ctx = context; | |
| } | |
| get width() { | |
| return this._width; | |
| } | |
| set width(width: number) { | |
| this._width = width; | |
| const ext = this.ctx.getExtension('STACKGL_resize_drawingbuffer'); | |
| ext.resize(width, this.height); | |
| } | |
| get height() { | |
| return this._height; | |
| } | |
| set height(height: number) { | |
| this._height = height; | |
| const ext = this.ctx.getExtension('STACKGL_resize_drawingbuffer'); | |
| ext.resize(this.width, height); | |
| } | |
| /*// @ts-ignore | |
| getContext(type, options) { | |
| if (type === 'webgl') { | |
| this.ctx = createContext(200, 300, options); | |
| return this.ctx; | |
| } | |
| return null as WebGLRenderingContext; | |
| }*/ | |
| addEventListener(evt: 'webglcontextlost') {} | |
| async toBuffer() { | |
| const pixels = new Uint8Array(this.width * this.height * 4); | |
| this.ctx.readPixels(0, 0, this.width, this.height, this.ctx.RGBA, this.ctx.UNSIGNED_BYTE, pixels); | |
| const canvas = new Canvas(this.width, this.height); | |
| const ctx = canvas.getContext('2d'); | |
| ctx.putImageData(new ImageData(new Uint8ClampedArray(pixels), this.width, this.height), 0, 0); | |
| return canvas.toBuffer('png'); | |
| } | |
| } | |
| interface GaugeRendererInitOptions { | |
| source: HTMLImageElement; | |
| gauge: HTMLImageElement; | |
| } | |
| const gl = createContext(512, 192, { antialias: true }); | |
| export default class GaugeRenderer { | |
| source: Image; // base64 string | |
| gauge: Image; | |
| canvas: GLCanvas; | |
| program: WebGLProgram; | |
| texture: WebGLTexture; | |
| pos: WebGLBuffer; | |
| uv: WebGLBuffer; | |
| ib: WebGLBuffer; | |
| primitiveCount: number; | |
| width: number = 256; | |
| height: number = 192; | |
| constructor(options: GaugeRendererInitOptions) { | |
| this.source = options.source; | |
| this.gauge = options.gauge; | |
| this.canvas = new GLCanvas(gl); | |
| gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT); | |
| gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT); | |
| gl.getExtension('OES_element_index_uint'); | |
| // initial program | |
| this.program = gl.createProgram(); | |
| const vsShader = gl.createShader(gl.VERTEX_SHADER); | |
| gl.shaderSource(vsShader, SHADER_SOURCE.vs); | |
| gl.compileShader(vsShader); | |
| const logVs = gl.getShaderInfoLog(vsShader); | |
| logVs && console.warn('vs log:', logVs); | |
| const fsShader = gl.createShader(gl.FRAGMENT_SHADER); | |
| gl.shaderSource(fsShader, SHADER_SOURCE.fs); | |
| gl.compileShader(fsShader); | |
| const logFs = gl.getShaderInfoLog(fsShader); | |
| logFs && console.warn('fs log:', logFs); | |
| gl.attachShader(this.program, vsShader); | |
| gl.attachShader(this.program, fsShader); | |
| gl.linkProgram(this.program); | |
| const logProgram = gl.getProgramInfoLog(this.program); | |
| logProgram && console.warn('program log:', logProgram); | |
| gl.deleteShader(vsShader); | |
| gl.deleteShader(fsShader); | |
| const { name: nameModelView } = gl.getActiveUniform(this.program, 0); | |
| const modelMat = gl.getUniformLocation(this.program, nameModelView); | |
| const { name: nameProj } = gl.getActiveUniform(this.program, 1); | |
| const projMat = gl.getUniformLocation(this.program, nameProj); | |
| const { name: nameUV } = gl.getActiveUniform(this.program, 2); | |
| const uvMat = gl.getUniformLocation(this.program, nameUV); | |
| const { name: nameDiffuse } = gl.getActiveUniform(this.program, 3); | |
| const diffuse = gl.getUniformLocation(this.program, nameDiffuse); | |
| const { name: nameOpacity } = gl.getActiveUniform(this.program, 4); | |
| const opacity = gl.getUniformLocation(this.program, nameOpacity); | |
| const { name: nameMap } = gl.getActiveUniform(this.program, 5); | |
| const map = gl.getUniformLocation(this.program, nameMap); | |
| gl.useProgram(this.program); | |
| gl.uniformMatrix4fv( | |
| projMat, | |
| false, | |
| //new Float32Array([0.0026385225355625153, 0, 0, 0, 0, -0.010416666977107525, 0, 0, 0, 0, -0.20202019810676575, 0, 0, 0, -1.0202020406723022, 1]) | |
| new Float32Array([0.002739726100116968, 0, 0, 0, 0, 0.010416666977107525, 0, 0, 0, 0, -0.20202019810676575, 0, 0, 0, -1.0202020406723022, 1]) | |
| ); | |
| gl.uniformMatrix4fv(modelMat, false, new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, -1, 1])); | |
| gl.uniformMatrix3fv(uvMat, false, new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1])); | |
| gl.uniform3f(diffuse, 1, 1, 1); | |
| gl.uniform1f(opacity, 1); | |
| gl.uniform1i(map, 0); | |
| // texture | |
| this.texture = gl.createTexture(); | |
| gl.activeTexture(gl.TEXTURE0); | |
| gl.bindTexture(gl.TEXTURE_2D, this.texture); | |
| gl.pixelStorei(37440, true); | |
| gl.pixelStorei(37441, false); | |
| gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4); | |
| gl.pixelStorei(37443, 0); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); | |
| gl.disable(gl.CULL_FACE); | |
| gl.depthMask(true); | |
| gl.colorMask(true, true, true, true); | |
| gl.disable(gl.STENCIL_TEST); | |
| gl.disable(gl.POLYGON_OFFSET_FILL); | |
| gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE); | |
| // buffers | |
| this.pos = gl.createBuffer(); | |
| this.uv = gl.createBuffer(); | |
| this.ib = gl.createBuffer(); | |
| const iPos = gl.getAttribLocation(this.program, 'position'); | |
| const iUV = gl.getAttribLocation(this.program, 'uv'); | |
| //console.log('indices:', iPos, iUV); | |
| gl.enableVertexAttribArray(iPos); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, this.pos); | |
| gl.vertexAttribPointer(iPos, 3, gl.FLOAT, false, 0, 0); | |
| gl.enableVertexAttribArray(iUV); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, this.uv); | |
| gl.vertexAttribPointer(iUV, 2, gl.FLOAT, false, 0, 0); | |
| } | |
| updateMaterial({ width = null, sw = this.width, sh = this.height } = {}) { | |
| if (sw !== this.width || sh !== this.height) { | |
| if (Number.isFinite(width)) { | |
| this.width = width; | |
| } else { | |
| this.width = Math.round((this.height * sw) / sh); | |
| } | |
| this.canvas.width = this.width; | |
| this.canvas.height = this.height; | |
| gl.viewport(0, 0, this.width, this.height); | |
| const projMat = gl.getUniformLocation(this.program, 'projectionMatrix'); | |
| gl.uniformMatrix4fv( | |
| projMat, | |
| false, | |
| new Float32Array([2 / this.width, 0, 0, 0, 0, 2 / this.height, 0, 0, 0, 0, -0.20202019810676575, 0, 0, 0, -1.0202020406723022, 1]) | |
| ); | |
| } | |
| // image to canvas | |
| const sourceCanvas = new Canvas(this.source.width, this.source.height); | |
| sourceCanvas.getContext('2d').drawImage(this.source, 0, 0); | |
| gl.bindTexture(gl.TEXTURE_2D, this.texture); | |
| gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, sourceCanvas as any); | |
| gl.generateMipmap(gl.TEXTURE_2D); | |
| } | |
| updateGeometry(baseY = null) { | |
| const { width, height } = this.gauge; | |
| const canvas = new Canvas(width, height); | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(this.gauge, 0, 0); | |
| const { data: buffer } = ctx.getImageData(0, 0, width, height); | |
| const xFactor = this.width / width; | |
| baseY = Math.round(Number.isFinite(baseY) ? baseY : height / 2); | |
| baseY = Math.max(0, Math.min(height - 1, baseY)); | |
| const propertyArray = Array(height) | |
| .fill(null) | |
| .map((_, y) => | |
| Array(width) | |
| .fill(null) | |
| .map((_, x) => ({ | |
| uv: [(x + 0.5) / width, 1 - (y + 0.5) / height], | |
| position: [(x - width / 2) * xFactor, (buffer[(y * width + x) * 4] + buffer[(y * width + x) * 4 + 2] / 256 - 128) / xFactor, 0], | |
| })) | |
| ); | |
| // integral X by K | |
| for (let y = baseY; y > 0; --y) { | |
| for (let x = 0; x < width; ++x) | |
| propertyArray[y - 1][x].position[0] = propertyArray[y][x].position[0] - ((buffer[(y * width + x) * 4 + 1] - 128) * xFactor) / 127; | |
| } | |
| for (let y = baseY + 1; y < height; ++y) { | |
| for (let x = 0; x < width; ++x) | |
| propertyArray[y][x].position[0] = propertyArray[y - 1][x].position[0] + ((buffer[((y - 1) * width + x) * 4 + 1] - 128) * xFactor) / 127; | |
| } | |
| const uvs = cc(cc(propertyArray).map((p) => p.uv)); | |
| const positions = cc(cc(propertyArray).map((p) => p.position)); | |
| const faces = Array(height - 1) | |
| .fill(null) | |
| .map((_, y) => | |
| Array(width - 1) | |
| .fill(null) | |
| .map((_, x) => [y * width + x, y * width + x + 1, (y + 1) * width + x, (y + 1) * width + x, (y + 1) * width + x + 1, y * width + x + 1]) | |
| ); | |
| const indices = cc(cc(faces)); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, this.pos); | |
| gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, this.uv); | |
| gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(uvs), gl.STATIC_DRAW); | |
| gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ib); | |
| gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW); | |
| this.primitiveCount = indices.length; | |
| } | |
| render() { | |
| gl.clearColor(1, 1, 1, 1); | |
| gl.clear(gl.COLOR_BUFFER_BIT); | |
| //gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ib); | |
| gl.drawElements(gl.TRIANGLES, this.primitiveCount, gl.UNSIGNED_INT, 0); | |
| return this.canvas.toBuffer(); | |
| } | |
| dispose() { | |
| gl.deleteBuffer(this.pos); | |
| gl.deleteBuffer(this.uv); | |
| gl.deleteBuffer(this.ib); | |
| gl.deleteProgram(this.program); | |
| gl.deleteTexture(this.texture); | |
| } | |
| } | |
| const gaugeRenderer = new GaugeRenderer({ | |
| source: new Image(), | |
| gauge: new Image(), | |
| }); | |
| export const renderGaugeImage = async (sourceURL: string | Buffer, gaugeURL: string | Buffer, baseY?: number) => { | |
| const source = await loadImage(sourceURL); | |
| const gauge = await loadImage(gaugeURL); | |
| gaugeRenderer.source = source; | |
| gaugeRenderer.gauge = gauge; | |
| gaugeRenderer.updateMaterial({ | |
| width: gauge.width, | |
| sw: source.width, | |
| sh: source.height, | |
| }); | |
| gaugeRenderer.updateGeometry(baseY); | |
| console.log(process.memoryUsage().heapUsed); | |
| return { | |
| buffer: await gaugeRenderer.render(), | |
| size: { | |
| width: gaugeRenderer.width, | |
| height: gaugeRenderer.height, | |
| }, | |
| }; | |
| }; | |
| // renderGaugeImage('./images/source.png', './images/gauge.png'); | |