starry / backend /libs /gauge-renderer-three.ts
k-l-lambda's picture
feat: add Python ML services (CPU mode) with model download
2b7aae2
import { Canvas, loadImage } from 'skia-canvas';
import * as THREE from './three/Three';
import gl from 'gl';
// threejs内部使用了OffscreenCanvas
(globalThis as any).OffscreenCanvas = (globalThis as any).OffscreenCanvas || Canvas;
const cc = <T>(a: T[][]): T[] => {
const result: T[] = [];
for (const x of a) {
for (const e of x) result.push(e);
}
return result;
};
class GLCanvas {
ctx: ReturnType<typeof gl>;
width: number = 256;
height: number = 192;
resizeBuffer: number[];
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
getContext(type, options) {
if (type === 'webgl') {
this.ctx = this.ctx || gl(this.width, this.height, options);
return this.ctx;
}
return null as WebGLRenderingContext;
}
addEventListener(evt: 'webglcontextlost') {}
removeEventListener(evt: any) {}
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({ data: new Uint8ClampedArray(pixels), width: this.width, height: this.height }, 0, 0);
return canvas.toBuffer('png');
}
}
export const renderGaugeImage = async (sourceURL: string | Buffer, gaugeURL: string | Buffer, baseY: number) => {
const source = await loadImage(sourceURL);
const gauge = await loadImage(gaugeURL);
const { width: gaugeWidth, height: gaugeHeight } = gauge;
const { width: srcWidth, height: srcHeight } = source;
let width: number = 256;
let height: number = 192;
if (width !== srcWidth || height !== srcHeight) {
if (Number.isFinite(gaugeWidth)) {
width = gaugeWidth;
} else {
width = Math.round((height * srcWidth) / srcHeight);
}
}
const camera = new THREE.OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 0.1, 10);
camera.position.set(0, 0, 1);
camera.up.set(0, 1, 0);
camera.lookAt(0, 0, 0);
const ctx = new Canvas(gaugeWidth, gaugeHeight).getContext('2d');
ctx.drawImage(gauge, 0, 0);
const { data: buff } = ctx.getImageData(0, 0, gaugeWidth, gaugeHeight);
const xFactor = width / gaugeWidth;
baseY = Math.round(Number.isFinite(baseY) ? baseY : gaugeHeight / 2);
baseY = Math.max(0, Math.min(gaugeHeight - 1, baseY));
const propertyArray = Array(gaugeHeight)
.fill(null)
.map((_, y) =>
Array(gaugeWidth)
.fill(null)
.map((_, x) => ({
uv: [(x + 0.5) / gaugeWidth, 1 - (y + 0.5) / gaugeHeight],
position: [(x - gaugeWidth / 2) * xFactor, (buff[(y * gaugeWidth + x) * 4] + buff[(y * gaugeWidth + x) * 4 + 2] / 256 - 128) / xFactor, 0],
}))
);
// integral X by K
for (let y = baseY; y > 0; --y) {
for (let x = 0; x < gaugeWidth; ++x)
propertyArray[y - 1][x].position[0] = propertyArray[y][x].position[0] - ((buff[(y * gaugeWidth + x) * 4 + 1] - 128) * xFactor) / 127;
}
for (let y = baseY + 1; y < gaugeHeight; ++y) {
for (let x = 0; x < gaugeWidth; ++x)
propertyArray[y][x].position[0] = propertyArray[y - 1][x].position[0] + ((buff[((y - 1) * gaugeWidth + 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 geometry = new THREE.BufferGeometry();
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
const faces = Array(gaugeHeight - 1)
.fill(null)
.map((_, y) =>
Array(gaugeWidth - 1)
.fill(null)
.map((_, x) => [
y * gaugeWidth + x,
y * gaugeWidth + x + 1,
(y + 1) * gaugeWidth + x,
(y + 1) * gaugeWidth + x,
(y + 1) * gaugeWidth + x + 1,
y * gaugeWidth + x + 1,
])
);
const indices = cc(cc(faces));
geometry.setIndex(indices);
const material = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide,
map: new THREE.Texture(source),
});
material.map.needsUpdate = true;
material.needsUpdate = true;
const scene = new THREE.Scene();
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);
const glCanvas = new GLCanvas(width, height);
const renderer = new THREE.WebGLRenderer({
antialias: true,
canvas: glCanvas as any,
alpha: false,
});
renderer.render(scene, camera);
renderer.dispose();
return {
buffer: await glCanvas.toBuffer(),
size: {
width,
height,
},
};
};