rkgk/static/brush-renderer.js

212 lines
6.2 KiB
JavaScript

import { compileProgram, orthographicProjection } from "rkgk/webgl.js";
const linesVertexShader = `#version 300 es
precision highp float;
uniform mat4 u_projection;
uniform vec2 u_translation;
layout (location = 0) in vec2 a_position;
// Instance
layout (location = 1) in vec4 a_line; // (x1, y1, x2, y2)
layout (location = 2) in vec4 a_color;
layout (location = 3) in vec2 a_properties; // (thickness, hardness)
out vec2 vf_localPosition;
out vec4 vf_line;
out vec4 vf_color;
out vec2 vf_properties;
void main() {
float thickness = a_properties.x;
vec2 from = a_line.xy;
vec2 to = a_line.zw;
vec2 direction = normalize(to - from);
if (to == from)
direction = vec2(1.0, 0.0);
// Extrude forward for caps
from -= direction * (thickness / 2.0);
to += direction * (thickness / 2.0);
vec2 xAxis = to - from;
vec2 yAxis = vec2(-direction.y, direction.x) * thickness;
vec2 localPosition = from + xAxis * a_position.x + yAxis * a_position.y;
vec4 screenPosition = vec4(localPosition + u_translation, 0.0, 1.0);
vec4 scenePosition = screenPosition * u_projection;
gl_Position = scenePosition;
vf_localPosition = localPosition;
vf_line = a_line;
vf_color = a_color;
vf_properties = a_properties;
}
`;
const linesFragmentShader = `#version 300 es
precision highp float;
in vec2 vf_localPosition;
in vec4 vf_line;
in vec4 vf_color;
in vec2 vf_properties;
out vec4 f_color;
// https://iquilezles.org/articles/distfunctions2d/
float segmentSdf(vec2 uv, vec2 a, vec2 b) {
vec2 uva = uv - a;
vec2 ba = b - a;
float h = clamp(dot(uva, ba) / dot(ba, ba), 0.0, 1.0);
return length(uva - ba * h);
}
void main() {
float thickness = vf_properties.x;
float hardness = vf_properties.y;
float halfSoftness = (1.0 - hardness) / 2.0;
vec2 uv = vf_localPosition;
float alpha = -(segmentSdf(uv, vf_line.xy, vf_line.zw) - thickness) / thickness;
if (hardness > 0.999)
alpha = step(0.5, alpha);
else
alpha = smoothstep(0.5 - halfSoftness, 0.5001 + halfSoftness, alpha);
f_color = vec4(vec3(1.0), alpha) * vf_color;
}
`;
const linesMaxInstances = 1;
const lineInstanceSize = 12;
const lineDataBufferSize = lineInstanceSize * linesMaxInstances;
export class BrushRenderer {
#translation = { x: 0, y: 0 };
constructor(gl, canvasSource) {
this.gl = gl;
this.canvasSource = canvasSource;
console.group("construct BrushRenderer");
// Lines
let linesProgramId = compileProgram(gl, linesVertexShader, linesFragmentShader);
this.linesProgram = {
id: linesProgramId,
u_projection: gl.getUniformLocation(linesProgramId, "u_projection"),
u_translation: gl.getUniformLocation(linesProgramId, "u_translation"),
};
this.linesVao = gl.createVertexArray();
this.linesVbo = gl.createBuffer();
gl.bindVertexArray(this.linesVao);
gl.bindBuffer(gl.ARRAY_BUFFER, this.linesVbo);
const lineRect = new Float32Array(
// prettier-ignore
[
0, -0.5,
1, -0.5,
0, 0.5,
1, -0.5,
1, 0.5,
0, 0.5,
],
);
this.linesVboData = new Float32Array(lineRect.length + lineDataBufferSize);
this.linesVboData.set(lineRect, 0);
this.linesInstanceData = this.linesVboData.subarray(lineRect.length);
gl.bufferData(gl.ARRAY_BUFFER, this.linesVboData, gl.DYNAMIC_DRAW, 0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 2 * 4, 0); // a_position
gl.vertexAttribPointer(1, 4, gl.FLOAT, false, lineInstanceSize, lineRect.byteLength); // a_line
gl.vertexAttribPointer(
2, // a_color
4,
gl.FLOAT,
false,
lineInstanceSize,
lineRect.byteLength + 4 * 4,
);
gl.vertexAttribPointer(
3, // a_properties
2,
gl.FLOAT,
false,
lineInstanceSize,
lineRect.byteLength + 4 * 4 * 2,
);
for (let i = 0; i < 4; ++i) gl.enableVertexAttribArray(i);
for (let i = 1; i < 4; ++i) gl.vertexAttribDivisor(i, 1);
console.debug("pipeline lines", {
linesVao: this.linesVao,
linesVbo: this.linesVbo,
linesVboSize: this.linesVboData.byteLength,
linesInstanceDataOffset: this.linesInstanceData.byteOffset,
});
gl.bindVertexArray(null);
console.groupEnd();
}
#drawLines(instanceCount) {
let gl = this.gl;
gl.bindVertexArray(this.linesVao);
gl.bindBuffer(gl.ARRAY_BUFFER, this.linesVbo);
gl.bufferSubData(
gl.ARRAY_BUFFER,
this.linesInstanceData.byteOffset,
this.linesInstanceData.subarray(0, instanceCount * lineInstanceSize),
);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, instanceCount);
}
setTranslation(x, y) {
this.#translation.x = x;
this.#translation.y = y;
}
stroke(canvas, r, g, b, a, thickness, x1, y1, x2, y2) {
let gl = this.gl;
let viewport = this.canvasSource.useCanvas(gl, canvas);
gl.useProgram(this.linesProgram.id);
gl.uniformMatrix4fv(
this.linesProgram.u_projection,
false,
orthographicProjection(0, viewport.width, viewport.height, 0, -1, 1),
);
gl.uniform2f(this.linesProgram.u_translation, this.#translation.x, this.#translation.y);
gl.enable(gl.BLEND);
let instances = this.linesInstanceData;
instances[0] = x1;
instances[1] = y1;
instances[2] = x2;
instances[3] = y2;
instances[4] = r / 255;
instances[5] = g / 255;
instances[6] = b / 255;
instances[7] = a / 255;
instances[8] = thickness;
instances[9] = 1; // hardness
this.#drawLines(1);
this.canvasSource.resetCanvas(gl);
return true;
}
}