|
|
|
const simple_vs_src = `#version 300 es
|
|
|
|
in vec2 a_pos;
|
|
|
|
|
|
|
|
uniform vec2 u_scale;
|
|
|
|
uniform vec2 u_res;
|
|
|
|
uniform vec2 u_translation;
|
|
|
|
|
|
|
|
out vec2 v_uv;
|
|
|
|
flat out int v_quad_id;
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res;
|
|
|
|
vec2 screen02 = screen01 * 2.0;
|
|
|
|
screen02.y = 2.0 - screen02.y;
|
|
|
|
|
|
|
|
int vertex_index = gl_VertexID % 6;
|
|
|
|
|
|
|
|
if (vertex_index == 0) {
|
|
|
|
v_uv = vec2(0.0, 0.0);
|
|
|
|
} else if (vertex_index == 1 || vertex_index == 5) {
|
|
|
|
v_uv = vec2(1.0, 0.0);
|
|
|
|
} else if (vertex_index == 2 || vertex_index == 4) {
|
|
|
|
v_uv = vec2(0.0, 1.0);
|
|
|
|
} else {
|
|
|
|
v_uv = vec2(1.0, 1.0);
|
|
|
|
}
|
|
|
|
|
|
|
|
v_quad_id = gl_VertexID / 6;
|
|
|
|
|
|
|
|
gl_Position = vec4(screen02 - 1.0, 0.0, 1.0);
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const simple_fs_src = `#version 300 es
|
|
|
|
precision highp float;
|
|
|
|
in vec2 v_uv;
|
|
|
|
flat in int v_quad_id;
|
|
|
|
layout(location = 0) out vec4 FragColor;
|
|
|
|
void main() {
|
|
|
|
vec2 pixel = fwidth(v_uv);
|
|
|
|
vec2 border = 2.0 * pixel;
|
|
|
|
|
|
|
|
if (border.x <= v_uv.x && v_uv.x <= 1.0 - border.x && border.y <= v_uv.y && v_uv.y <= 1.0 - border.y) {
|
|
|
|
discard;
|
|
|
|
} else {
|
|
|
|
vec3 color = vec3(float(v_quad_id * 869363 % 255) / 255.0, float(v_quad_id * 278975 % 255) / 255.0, float(v_quad_id * 587286 % 255) / 255.0);
|
|
|
|
float alpha = 0.5;
|
|
|
|
FragColor = vec4(color * alpha, alpha);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const opaque_vs_src = `#version 300 es
|
|
|
|
in vec3 a_pos; // .z is radius
|
|
|
|
in vec4 a_line;
|
|
|
|
|
|
|
|
in int a_stroke_id;
|
|
|
|
|
|
|
|
uniform vec2 u_scale;
|
|
|
|
uniform vec2 u_res;
|
|
|
|
uniform vec2 u_translation;
|
|
|
|
|
|
|
|
uniform int u_stroke_count;
|
|
|
|
|
|
|
|
flat out int v_stroke_id;
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
// Do not inflate quad (as opposed to the full sdf shader), thus only leaving the opaque part
|
|
|
|
|
|
|
|
// Shrink to not include the caps
|
|
|
|
vec2 line_dir = normalize(a_line.zw - a_line.xy);
|
|
|
|
|
|
|
|
int vertex_index = gl_VertexID % 4;
|
|
|
|
vec2 pos = a_pos.xy;
|
|
|
|
|
|
|
|
if (vertex_index == 0 || vertex_index == 2) {
|
|
|
|
// vertices on the "beginning" side of the line
|
|
|
|
pos.xy += line_dir * a_pos.z / 2.0;
|
|
|
|
} else {
|
|
|
|
// on the "ending" side of the line
|
|
|
|
pos.xy -= line_dir * a_pos.z / 2.0;
|
|
|
|
}
|
|
|
|
|
|
|
|
vec2 screen01 = (pos * u_scale + u_translation) / u_res;
|
|
|
|
vec2 screen02 = screen01 * 2.0;
|
|
|
|
screen02.y = 2.0 - screen02.y;
|
|
|
|
|
|
|
|
v_stroke_id = a_stroke_id;
|
|
|
|
|
|
|
|
gl_Position = vec4(screen02 - 1.0, (float(a_stroke_id) / float(u_stroke_count)) * 2.0 - 1.0, 1.0);
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const nop_fs_src = `#version 300 es
|
|
|
|
precision highp float;
|
|
|
|
flat in int v_stroke_id;
|
|
|
|
layout(location = 0) out vec4 FragColor;
|
|
|
|
void main() {
|
|
|
|
vec3 color = vec3(float(v_stroke_id * 3245 % 255) / 255.0, float(v_stroke_id * 7343 % 255) / 255.0, float(v_stroke_id * 5528 % 255) / 255.0);
|
|
|
|
FragColor = vec4(color, 1.0);
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const sdf_vs_src = `#version 300 es
|
|
|
|
in vec2 a_a; // point from
|
|
|
|
in vec2 a_b; // point to
|
|
|
|
in float a_radius;
|
|
|
|
in int a_stroke_id;
|
|
|
|
|
|
|
|
uniform vec2 u_scale;
|
|
|
|
uniform vec2 u_res;
|
|
|
|
uniform vec2 u_translation;
|
|
|
|
uniform int u_stroke_count;
|
|
|
|
uniform int u_stroke_texture_size;
|
|
|
|
|
|
|
|
uniform highp usampler2D u_stroke_data;
|
|
|
|
|
|
|
|
out vec4 v_line;
|
|
|
|
out vec2 v_texcoord;
|
|
|
|
out vec3 v_color;
|
|
|
|
|
|
|
|
flat out float v_thickness;
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
vec2 screen02;
|
|
|
|
float apron = 1.0; // google "futanari inflation rule 34"
|
|
|
|
|
|
|
|
int stroke_data_y = a_stroke_id / u_stroke_texture_size;
|
|
|
|
int stroke_data_x = a_stroke_id % u_stroke_texture_size;
|
|
|
|
|
|
|
|
uvec4 stroke_data = texelFetch(u_stroke_data, ivec2(stroke_data_x, stroke_data_y), 0);
|
|
|
|
float radius = float(stroke_data.w);
|
|
|
|
|
|
|
|
vec2 line_dir = normalize(a_b - a_a);
|
|
|
|
vec2 up_dir = vec2(line_dir.y, -line_dir.x);
|
|
|
|
vec2 pixel = vec2(2.0) / u_res * apron;
|
|
|
|
float rscale = apron / u_scale.x;
|
|
|
|
|
|
|
|
int vertex_index = gl_VertexID % 6;
|
|
|
|
|
|
|
|
vec2 outwards;
|
|
|
|
vec2 origin;
|
|
|
|
|
|
|
|
if (vertex_index == 0) {
|
|
|
|
// "top left" aka "p1"
|
|
|
|
origin = a_a;
|
|
|
|
outwards = up_dir - line_dir;
|
|
|
|
} else if (vertex_index == 1 || vertex_index == 5) {
|
|
|
|
// "top right" aka "p2"
|
|
|
|
origin = a_b;
|
|
|
|
outwards = up_dir + line_dir;
|
|
|
|
} else if (vertex_index == 2 || vertex_index == 4) {
|
|
|
|
// "bottom left" aka "p3"
|
|
|
|
origin = a_a;
|
|
|
|
outwards = -up_dir - line_dir;
|
|
|
|
} else {
|
|
|
|
// "bottom right" aka "p4"
|
|
|
|
origin = a_b;
|
|
|
|
outwards = -up_dir + line_dir;
|
|
|
|
}
|
|
|
|
|
|
|
|
vec2 pos = origin + normalize(outwards) * radius;
|
|
|
|
screen02 = (pos.xy * u_scale + u_translation) / u_res * 2.0 + outwards * pixel;
|
|
|
|
v_texcoord = pos.xy + outwards * rscale;
|
|
|
|
|
|
|
|
screen02.y = 2.0 - screen02.y;
|
|
|
|
|
|
|
|
v_line = vec4(a_a, a_b);
|
|
|
|
v_thickness = radius;
|
|
|
|
v_color = vec3(stroke_data.xyz) / 255.0;
|
|
|
|
|
|
|
|
if (a_stroke_id >> 31 != 0) {
|
|
|
|
screen02 += vec2(100.0); // shift offscreen
|
|
|
|
}
|
|
|
|
|
|
|
|
gl_Position = vec4(screen02 - 1.0, (float(a_stroke_id) / float(u_stroke_count)) * 2.0 - 1.0, 1);
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const sdf_fs_src = `#version 300 es
|
|
|
|
precision highp float;
|
|
|
|
|
|
|
|
uniform int u_debug_mode;
|
|
|
|
|
|
|
|
in vec4 v_line;
|
|
|
|
in vec2 v_texcoord;
|
|
|
|
in vec3 v_color;
|
|
|
|
|
|
|
|
flat in float v_thickness;
|
|
|
|
|
|
|
|
layout(location = 0) out vec4 FragColor;
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
if (u_debug_mode == 0) {
|
|
|
|
vec2 a = v_line.xy;
|
|
|
|
vec2 b = v_line.zw;
|
|
|
|
|
|
|
|
vec2 pa = v_texcoord - a.xy, ba = b.xy - a.xy;
|
|
|
|
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
|
|
|
|
float dist = length(pa - ba * h) - v_thickness / 2.0;
|
|
|
|
|
|
|
|
float fade = 0.5 * length(fwidth(v_texcoord));
|
|
|
|
float alpha = 1.0 - smoothstep(-fade, fade, dist);
|
|
|
|
|
|
|
|
FragColor = vec4(v_color * alpha, alpha);
|
|
|
|
} else {
|
|
|
|
FragColor = vec4(0.2, 0.0, 0.0, 0.2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const tquad_vs_src = `#version 300 es
|
|
|
|
in vec2 a_pos;
|
|
|
|
in vec2 a_texcoord;
|
|
|
|
|
|
|
|
uniform vec2 u_scale;
|
|
|
|
uniform vec2 u_res;
|
|
|
|
uniform vec2 u_translation;
|
|
|
|
|
|
|
|
out vec2 v_texcoord;
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res;
|
|
|
|
vec2 screen02 = screen01 * 2.0;
|
|
|
|
screen02.y = 2.0 - screen02.y;
|
|
|
|
vec2 screen11 = screen02 - 1.0;
|
|
|
|
v_texcoord = a_texcoord;
|
|
|
|
gl_Position = vec4(screen11, 0, 1);
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const tquad_fs_src = `#version 300 es
|
|
|
|
precision highp float;
|
|
|
|
|
|
|
|
in vec2 v_texcoord;
|
|
|
|
|
|
|
|
uniform sampler2D u_texture;
|
|
|
|
uniform bool u_outline;
|
|
|
|
|
|
|
|
layout(location = 0) out vec4 FragColor;
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
if (!u_outline) {
|
|
|
|
FragColor = texture(u_texture, v_texcoord);
|
|
|
|
} else {
|
|
|
|
FragColor = mix(texture(u_texture, v_texcoord), vec4(0.7, 0.7, 0.95, 1), 0.5);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
|
|
|
function init_webgl(state, context) {
|
|
|
|
context.canvas = document.querySelector('#c');
|
|
|
|
context.gl = context.canvas.getContext('webgl2', {
|
|
|
|
'preserveDrawingBuffer': true,
|
|
|
|
'desynchronized': true,
|
|
|
|
'antialias': false,
|
|
|
|
});
|
|
|
|
|
|
|
|
const gl = context.gl;
|
|
|
|
|
|
|
|
gl.enable(gl.BLEND);
|
|
|
|
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
|
|
|
|
|
|
gl.enable(gl.DEPTH_TEST);
|
|
|
|
gl.depthFunc(gl.GEQUAL);
|
|
|
|
|
|
|
|
context.gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query_webgl2');
|
|
|
|
if (context.gpu_timer_ext === null) {
|
|
|
|
context.gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query');
|
|
|
|
}
|
|
|
|
|
|
|
|
const quad_vs = create_shader(gl, gl.VERTEX_SHADER, tquad_vs_src);
|
|
|
|
const quad_fs = create_shader(gl, gl.FRAGMENT_SHADER, tquad_fs_src);
|
|
|
|
|
|
|
|
const sdf_vs = create_shader(gl, gl.VERTEX_SHADER, sdf_vs_src);
|
|
|
|
const sdf_fs = create_shader(gl, gl.FRAGMENT_SHADER, sdf_fs_src);
|
|
|
|
|
|
|
|
const opaque_vs = create_shader(gl, gl.VERTEX_SHADER, opaque_vs_src);
|
|
|
|
const nop_fs = create_shader(gl, gl.FRAGMENT_SHADER, nop_fs_src);
|
|
|
|
|
|
|
|
const simple_vs = create_shader(gl, gl.VERTEX_SHADER, simple_vs_src);
|
|
|
|
const simple_fs = create_shader(gl, gl.FRAGMENT_SHADER, simple_fs_src);
|
|
|
|
|
|
|
|
context.programs['image'] = create_program(gl, quad_vs, quad_fs);
|
|
|
|
context.programs['debug'] = create_program(gl, simple_vs, simple_fs);
|
|
|
|
context.programs['sdf'] = {
|
|
|
|
'opaque': create_program(gl, opaque_vs, nop_fs),
|
|
|
|
'main': create_program(gl, sdf_vs, sdf_fs),
|
|
|
|
};
|
|
|
|
|
|
|
|
context.locations['debug'] = {
|
|
|
|
'a_pos': gl.getAttribLocation(context.programs['debug'], 'a_pos'),
|
|
|
|
|
|
|
|
'u_res': gl.getUniformLocation(context.programs['debug'], 'u_res'),
|
|
|
|
'u_scale': gl.getUniformLocation(context.programs['debug'], 'u_scale'),
|
|
|
|
'u_translation': gl.getUniformLocation(context.programs['debug'], 'u_translation'),
|
|
|
|
};
|
|
|
|
|
|
|
|
context.locations['sdf'] = {
|
|
|
|
'opaque': {
|
|
|
|
'a_pos': gl.getAttribLocation(context.programs['sdf'].opaque, 'a_pos'),
|
|
|
|
'a_line': gl.getAttribLocation(context.programs['sdf'].opaque, 'a_line'),
|
|
|
|
'a_stroke_id': gl.getAttribLocation(context.programs['sdf'].opaque, 'a_stroke_id'),
|
|
|
|
|
|
|
|
'u_res': gl.getUniformLocation(context.programs['sdf'].opaque, 'u_res'),
|
|
|
|
'u_scale': gl.getUniformLocation(context.programs['sdf'].opaque, 'u_scale'),
|
|
|
|
'u_translation': gl.getUniformLocation(context.programs['sdf'].opaque, 'u_translation'),
|
|
|
|
'u_stroke_count': gl.getUniformLocation(context.programs['sdf'].opaque, 'u_stroke_count'),
|
|
|
|
},
|
|
|
|
|
|
|
|
'main': {
|
|
|
|
'a_a': gl.getAttribLocation(context.programs['sdf'].main, 'a_a'),
|
|
|
|
'a_b': gl.getAttribLocation(context.programs['sdf'].main, 'a_b'),
|
|
|
|
'a_stroke_id': gl.getAttribLocation(context.programs['sdf'].main, 'a_stroke_id'),
|
|
|
|
|
|
|
|
'u_res': gl.getUniformLocation(context.programs['sdf'].main, 'u_res'),
|
|
|
|
'u_scale': gl.getUniformLocation(context.programs['sdf'].main, 'u_scale'),
|
|
|
|
'u_translation': gl.getUniformLocation(context.programs['sdf'].main, 'u_translation'),
|
|
|
|
'u_debug_mode': gl.getUniformLocation(context.programs['sdf'].main, 'u_debug_mode'),
|
|
|
|
'u_stroke_count': gl.getUniformLocation(context.programs['sdf'].main, 'u_stroke_count'),
|
|
|
|
'u_stroke_data': gl.getUniformLocation(context.programs['sdf'].main, 'u_stroke_data'),
|
|
|
|
'u_stroke_texture_size': gl.getUniformLocation(context.programs['sdf'].main, 'u_stroke_texture_size'),
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
for (let i = 0; i < context.lods.length; ++i) {
|
|
|
|
const level = context.lods[i];
|
|
|
|
level.data_buffer = gl.createBuffer();
|
|
|
|
level.index_buffer = gl.createBuffer();
|
|
|
|
}
|
|
|
|
|
|
|
|
context.buffers['debug'] = {
|
|
|
|
'b_packed': gl.createBuffer(),
|
|
|
|
};
|
|
|
|
|
|
|
|
context.buffers['sdf'] = {
|
|
|
|
'b_packed_dynamic': gl.createBuffer(),
|
|
|
|
'b_packed_dynamic_index': gl.createBuffer(),
|
|
|
|
'b_instance': gl.createBuffer(),
|
|
|
|
};
|
|
|
|
|
|
|
|
context.textures = {
|
|
|
|
'stroke_data': gl.createTexture(),
|
|
|
|
};
|
|
|
|
|
|
|
|
gl.bindTexture(gl.TEXTURE_2D, context.textures['stroke_data']);
|
|
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
|
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.stroke_texture_size, config.stroke_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, null);
|
|
|
|
|
|
|
|
const resize_canvas = (entries) => {
|
|
|
|
// https://www.khronos.org/webgl/wiki/HandlingHighDPI
|
|
|
|
const entry = entries[0];
|
|
|
|
|
|
|
|
let width;
|
|
|
|
let height;
|
|
|
|
|
|
|
|
if (entry.devicePixelContentBoxSize) {
|
|
|
|
width = entry.devicePixelContentBoxSize[0].inlineSize;
|
|
|
|
height = entry.devicePixelContentBoxSize[0].blockSize;
|
|
|
|
} else if (entry.contentBoxSize) {
|
|
|
|
// fallback for Safari that will not always be correct
|
|
|
|
width = Math.round(entry.contentBoxSize[0].inlineSize * devicePixelRatio);
|
|
|
|
height = Math.round(entry.contentBoxSize[0].blockSize * devicePixelRatio);
|
|
|
|
}
|
|
|
|
|
|
|
|
context.canvas.width = width;
|
|
|
|
context.canvas.height = height;
|
|
|
|
|
|
|
|
schedule_draw(state, context);
|
|
|
|
}
|
|
|
|
|
|
|
|
const resize_observer = new ResizeObserver(resize_canvas);
|
|
|
|
resize_observer.observe(context.canvas);
|
|
|
|
}
|
|
|
|
|
|
|
|
function create_shader(gl, type, source) {
|
|
|
|
const shader = gl.createShader(type);
|
|
|
|
|
|
|
|
gl.shaderSource(shader, source);
|
|
|
|
gl.compileShader(shader);
|
|
|
|
|
|
|
|
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
|
|
return shader;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.error(type, ':', gl.getShaderInfoLog(shader));
|
|
|
|
|
|
|
|
gl.deleteShader(shader);
|
|
|
|
}
|
|
|
|
|
|
|
|
function create_program(gl, vs, fs) {
|
|
|
|
const program = gl.createProgram();
|
|
|
|
|
|
|
|
gl.attachShader(program, vs);
|
|
|
|
gl.attachShader(program, fs);
|
|
|
|
gl.linkProgram(program);
|
|
|
|
|
|
|
|
if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
|
|
return program;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.error('link:', gl.getProgramInfoLog(program));
|
|
|
|
|
|
|
|
gl.deleteProgram(program);
|
|
|
|
}
|