starry / backend /libs /gauge-renderer.ts
k-l-lambda's picture
Initial deployment: frontend + omr-service + cluster-server + nginx proxy
6f1c297
/* 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');