Browse Source

Cull segments, switch to indexed drawing (only for static data for now)

ssao
A.Olokhtonov 1 year ago
parent
commit
46cd48fecd
  1. 6
      client/client_recv.js
  2. 17
      client/default.css
  3. 32
      client/index.html
  4. 71
      client/index.js
  5. 83
      client/math.js
  6. 12
      client/webgl_draw.js
  7. 11
      client/webgl_geometry.js
  8. 18
      client/webgl_shaders.js

6
client/client_recv.js

@ -304,8 +304,10 @@ async function handle_message(state, context, d) {
switch (message_type) { switch (message_type) {
case MESSAGE.JOIN: case MESSAGE.JOIN:
case MESSAGE.INIT: { case MESSAGE.INIT: {
state.server_lsn = des_u32(d); console.time('init');
state.online = true; state.online = true;
state.server_lsn = des_u32(d);
if (state.server_lsn > state.lsn) { if (state.server_lsn > state.lsn) {
// Server knows something that we don't // Server knows something that we don't
@ -359,6 +361,8 @@ async function handle_message(state, context, d) {
send_ack(event_count); send_ack(event_count);
sync_queue(state); sync_queue(state);
console.timeEnd('init');
break; break;
} }

17
client/default.css

@ -295,3 +295,20 @@ input[type=range]::-moz-range-track {
body.offline * { body.offline * {
pointer-events: none; pointer-events: none;
} }
.loader {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 128px;
height: 128px;
pointer-events: none;
background: rgba(0, 0, 0, 0.8);
border-radius: 10px;
transition: opacity .2s;
}
.loader.hidden {
opacity: 0;
}

32
client/index.html

@ -7,20 +7,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="shortcut icon" href="icons/favicon.svg" id="favicon"> <link rel="shortcut icon" href="icons/favicon.svg" id="favicon">
<link rel="stylesheet" type="text/css" href="default.css?v=58"> <link rel="stylesheet" type="text/css" href="default.css?v=65">
<script type="text/javascript" src="aux.js?v=58"></script> <script type="text/javascript" src="aux.js?v=65"></script>
<script type="text/javascript" src="math.js?v=58"></script> <script type="text/javascript" src="math.js?v=65"></script>
<script type="text/javascript" src="tools.js?v=58"></script> <script type="text/javascript" src="tools.js?v=65"></script>
<script type="text/javascript" src="webgl_geometry.js?v=58"></script> <script type="text/javascript" src="webgl_geometry.js?v=65"></script>
<script type="text/javascript" src="webgl_shaders.js?v=58"></script> <script type="text/javascript" src="webgl_shaders.js?v=65"></script>
<script type="text/javascript" src="webgl_listeners.js?v=58"></script> <script type="text/javascript" src="webgl_listeners.js?v=65"></script>
<script type="text/javascript" src="webgl_draw.js?v=58"></script> <script type="text/javascript" src="webgl_draw.js?v=65"></script>
<script type="text/javascript" src="index.js?v=58"></script> <script type="text/javascript" src="index.js?v=65"></script>
<script type="text/javascript" src="client_send.js?v=58"></script> <script type="text/javascript" src="client_send.js?v=65"></script>
<script type="text/javascript" src="client_recv.js?v=58"></script> <script type="text/javascript" src="client_recv.js?v=65"></script>
<script type="text/javascript" src="websocket.js?v=58"></script> <script type="text/javascript" src="websocket.js?v=65"></script>
</head> </head>
<body> <body>
<div class="main"> <div class="main">
@ -69,5 +69,13 @@
<div class="offline-toast hidden"> <div class="offline-toast hidden">
Whiteboard offline Whiteboard offline
</div> </div>
<div class="loader">
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg">
<!-- <circle cx="64" cy="64" r="32" fill="none" stroke="black" opacity="0.5" stroke-width="3" stroke-linecap="round"/> -->
<path id="spinner-path" fill="none" stroke="#aaaaaa" stroke-width="3" stroke-linecap="round"/>
</svg>
</div>
</body> </body>
</html> </html>

71
client/index.js

@ -1,3 +1,5 @@
// NEXT: pan with m3, place dot, cursor size and color, YELLOW and gray
document.addEventListener('DOMContentLoaded', main); document.addEventListener('DOMContentLoaded', main);
const config = { const config = {
@ -48,6 +50,71 @@ const MESSAGE = Object.freeze({
JOIN: 105, JOIN: 105,
}); });
// Source:
// https://stackoverflow.com/a/18473154
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
function describeArc(x, y, radius, startAngle, endAngle) {
var start = polarToCartesian(x, y, radius, endAngle);
var end = polarToCartesian(x, y, radius, startAngle);
var largeArcFlag = (Math.abs(endAngle - startAngle) % 360) <= 180 ? "0" : "1";
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
].join(" ");
return d;
}
let iii = 0;
let a_angel = 0;
let b_angel = 180;
let speed_a = 2;
let speed_b = 6;
let b_fast = true;
function start_spinner(state) {
const str = describeArc(64, 64, 32, a_angel, b_angel);
a_angel += speed_a;
b_angel += speed_b;
const diff = b_angel - a_angel;
if (diff > 320) {
speed_a = 6;
speed_b = 2;
} else if (diff < 40) {
speed_a = 2;
speed_b = 6;
}
// if ((speed_a === 1) && Math.abs(a_angel - b_angel) % 360 < 90) {
// speed_a = 3;
// speed_b = 1;
// } else if (Math.abs(a_angel - b_angel) % 360 > 180) {
// speed_a = 1;
// speed_b = 3;
// }
document.querySelector('#spinner-path').setAttribute('d', str);
if (!state.online) {
window.requestAnimationFrame(() => start_spinner(state));
} else {
document.querySelector('.loader').classList.add('hidden');
}
}
function main() { function main() {
const state = { const state = {
'online': false, 'online': false,
@ -126,13 +193,17 @@ function main() {
'textures': {}, 'textures': {},
'static_serializer': serializer_create(config.initial_static_bytes), 'static_serializer': serializer_create(config.initial_static_bytes),
'static_index_serializer': serializer_create(config.initial_static_bytes),
'dynamic_serializer': serializer_create(config.initial_dynamic_bytes), 'dynamic_serializer': serializer_create(config.initial_dynamic_bytes),
'dynamic_index_serializer': serializer_create(config.initial_dynamic_bytes),
'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0}, 'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0},
'active_image': null, 'active_image': null,
}; };
start_spinner(state);
const url = new URL(window.location.href); const url = new URL(window.location.href);
const parts = url.pathname.split('/'); const parts = url.pathname.split('/');

83
client/math.js

@ -156,3 +156,86 @@ function mid_v2(a, b) {
'y': (a.y + b.y) / 2.0, 'y': (a.y + b.y) / 2.0,
}; };
} }
function point_in_quad(p, quad_topleft, quad_bottomright) {
if ((quad_topleft.x <= p.x && p.x < quad_bottomright.x) && (quad_topleft.y <= p.y && p.y < quad_bottomright.y)) {
return true;
}
return false;
}
function segment_interesects_quad(a, b, quad_topleft, quad_bottomright, quad_topright, quad_bottomleft) {
if (point_in_quad(a, quad_topleft, quad_bottomright)) {
return true;
}
if (point_in_quad(b, quad_topleft, quad_bottomright)) {
return true;
}
if (segments_intersect(a, b, quad_topleft, quad_topright)) return true;
if (segments_intersect(a, b, quad_topright, quad_bottomright)) return true;
if (segments_intersect(a, b, quad_bottomright, quad_bottomleft)) return true;
if (segments_intersect(a, b, quad_bottomleft, quad_topleft)) return true;
return false;
}
function stroke_bbox(points) {
let min_x = points[0].x;
let max_x = min_x;
let min_y = points[0].y;
let max_y = min_y;
for (const p of points) {
min_x = Math.min(min_x, p.x);
min_y = Math.min(min_y, p.y);
max_x = Math.max(max_x, p.x);
max_y = Math.max(max_y, p.y);
}
return {'x1': min_x, 'y1': min_y, 'x2': max_x, 'y2': max_y};
}
function quad_onscreen(screen, bbox) {
if (screen.x1 < bbox.x2 && screen.x2 > bbox.x1 && screen.y2 > bbox.y1 && screen.y1 < bbox.y2) {
return true;
}
return false;
}
function segments_onscreen(state, context) {
const result = [];
const screen_topleft = screen_to_canvas(state, {'x': 0, 'y': 0});
const screen_bottomright = screen_to_canvas(state, {'x': context.canvas.width, 'y': context.canvas.height});
const screen_topright = { 'x': screen_bottomright.x, 'y': screen_topleft.y };
const screen_bottomleft = { 'x': screen_topleft.x, 'y': screen_bottomright.y };
const screen = {'x1': screen_topleft.x, 'y1': screen_topleft.y, 'x2': screen_bottomright.x, 'y2': screen_bottomright.y};
let head = 0;
for (let i = 0; i < state.events.length; ++i) {
const event = state.events[i];
if (event.type === EVENT.STROKE && !event.deleted) {
if (quad_onscreen(screen, event.bbox)) {
for (let j = 0; j < event.points.length - 1; ++j) {
const a = event.points[j + 0];
const b = event.points[j + 1];
if (segment_interesects_quad(a, b, screen_topleft, screen_bottomright, screen_topright, screen_bottomleft)) {
let base = head + j * 4;
// We draw quads as [1, 2, 3, 4, 3, 2]
result.push(base + 0, base + 1, base + 2);
result.push(base + 3, base + 2, base + 1);
}
}
}
head += (event.points.length - 1) * 4;
}
}
return result;
}

12
client/webgl_draw.js

@ -48,7 +48,7 @@ function draw(state, context) {
gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, config.bytes_per_point, 4 * 3 + 4 * 4); gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, config.bytes_per_point, 4 * 3 + 4 * 4);
if (context.need_static_allocate) { if (context.need_static_allocate) {
console.debug('static allocate'); if (config.debug_print) console.debug('static allocate');
gl.bufferData(gl.ARRAY_BUFFER, context.static_serializer.size, gl.DYNAMIC_DRAW); gl.bufferData(gl.ARRAY_BUFFER, context.static_serializer.size, gl.DYNAMIC_DRAW);
context.need_static_allocate = false; context.need_static_allocate = false;
context.static_upload_from = 0; context.static_upload_from = 0;
@ -56,7 +56,7 @@ function draw(state, context) {
} }
if (context.need_static_upload) { if (context.need_static_upload) {
console.debug('static upload'); if (config.debug_print) console.debug('static upload');
const upload_offset = context.static_upload_from; const upload_offset = context.static_upload_from;
const upload_size = context.static_serializer.offset - upload_offset; const upload_size = context.static_serializer.offset - upload_offset;
gl.bufferSubData(gl.ARRAY_BUFFER, upload_offset, new Uint8Array(context.static_serializer.buffer, upload_offset, upload_size)); gl.bufferSubData(gl.ARRAY_BUFFER, upload_offset, new Uint8Array(context.static_serializer.buffer, upload_offset, upload_size));
@ -64,7 +64,11 @@ function draw(state, context) {
context.static_upload_from = context.static_serializer.offset; context.static_upload_from = context.static_serializer.offset;
} }
gl.drawArrays(gl.TRIANGLES, 0, static_points); const indices = segments_onscreen(state, context);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers['b_packed_static_index']);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.DYNAMIC_DRAW);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_INT, 0);
} }
if (dynamic_points > 0) { if (dynamic_points > 0) {
@ -93,7 +97,7 @@ function draw(state, context) {
if (wait_status === gl.ALREADY_SIGNALED || wait_status === gl.CONDITION_SATISFIED) { if (wait_status === gl.ALREADY_SIGNALED || wait_status === gl.CONDITION_SATISFIED) {
const frametime_ms = frame_end - frame_start; const frametime_ms = frame_end - frame_start;
gl.deleteSync(sync); gl.deleteSync(sync);
console.debug(frametime_ms); if (config.debug_print) console.debug(frametime_ms);
} else { } else {
setTimeout(next_tick, 0); setTimeout(next_tick, 0);
} }

11
client/webgl_geometry.js

@ -16,10 +16,7 @@ function push_quad(s, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y, ax, ay, bx, by, th
push_point(s, p1x, p1y, ax, ay, bx, by, thickness, r, g, b); push_point(s, p1x, p1y, ax, ay, bx, by, thickness, r, g, b);
push_point(s, p2x, p2y, ax, ay, bx, by, thickness, r, g, b); push_point(s, p2x, p2y, ax, ay, bx, by, thickness, r, g, b);
push_point(s, p3x, p3y, ax, ay, bx, by, thickness, r, g, b); push_point(s, p3x, p3y, ax, ay, bx, by, thickness, r, g, b);
push_point(s, p4x, p4y, ax, ay, bx, by, thickness, r, g, b); push_point(s, p4x, p4y, ax, ay, bx, by, thickness, r, g, b);
push_point(s, p3x, p3y, ax, ay, bx, by, thickness, r, g, b);
push_point(s, p2x, p2y, ax, ay, bx, by, thickness, r, g, b);
} }
function push_stroke(s, stroke) { function push_stroke(s, stroke) {
@ -88,10 +85,12 @@ function geometry_prepare_stroke(state) {
return null; return null;
} }
const points = process_stroke(state, state.players[state.me].points);
return { return {
'color': state.players[state.me].color, 'color': state.players[state.me].color,
'width': state.players[state.me].width, 'width': state.players[state.me].width,
'points': process_stroke(state, state.players[state.me].points), 'points': points,
'user_id': state.me, 'user_id': state.me,
}; };
} }
@ -99,8 +98,10 @@ function geometry_prepare_stroke(state) {
function geometry_add_stroke(state, context, stroke) { function geometry_add_stroke(state, context, stroke) {
if (!state.online || !stroke) return; if (!state.online || !stroke) return;
stroke.bbox = stroke_bbox(stroke.points);
let bytes_left = context.static_serializer.size - context.static_serializer.offset; let bytes_left = context.static_serializer.size - context.static_serializer.offset;
let bytes_needed = stroke.points.length * 6 * config.bytes_per_point; let bytes_needed = stroke.points.length * 4 * config.bytes_per_point;
if (bytes_left < bytes_needed) { if (bytes_left < bytes_needed) {
const extend_to = Math.ceil((context.static_serializer.size + bytes_needed) * 1.62); const extend_to = Math.ceil((context.static_serializer.size + bytes_needed) * 1.62);

18
client/webgl_shaders.js

@ -23,17 +23,17 @@ const sdf_vs_src = `#version 300 es
vec2 up_dir = vec2(line_dir.y, -line_dir.x); vec2 up_dir = vec2(line_dir.y, -line_dir.x);
vec2 pixel = vec2(2.0) / u_res * apron; vec2 pixel = vec2(2.0) / u_res * apron;
int vertex_index = gl_VertexID % 6; int vertex_index = gl_VertexID % 4;
if (vertex_index == 0) { if (vertex_index == 0) {
// "top left" aka "p1" // "top left" aka "p1"
screen02 += up_dir * pixel - line_dir * pixel; screen02 += up_dir * pixel - line_dir * pixel;
v_texcoord = a_pos.xy + up_dir * 1.0 / u_scale - line_dir * 1.0 / u_scale; v_texcoord = a_pos.xy + up_dir * 1.0 / u_scale - line_dir * 1.0 / u_scale;
} else if (vertex_index == 1 || vertex_index == 5) { } else if (vertex_index == 1) {
// "top right" aka "p2" // "top right" aka "p2"
screen02 += up_dir * pixel + line_dir * pixel; screen02 += up_dir * pixel + line_dir * pixel;
v_texcoord = a_pos.xy + up_dir * 1.0 / u_scale + line_dir * 1.0 / u_scale; v_texcoord = a_pos.xy + up_dir * 1.0 / u_scale + line_dir * 1.0 / u_scale;
} else if (vertex_index == 2 || vertex_index == 4) { } else if (vertex_index == 2) {
// "bottom left" aka "p3" // "bottom left" aka "p3"
screen02 += -up_dir * pixel - line_dir * pixel; screen02 += -up_dir * pixel - line_dir * pixel;
v_texcoord = a_pos.xy - up_dir * 1.0 / u_scale - line_dir * 1.0 / u_scale; v_texcoord = a_pos.xy - up_dir * 1.0 / u_scale - line_dir * 1.0 / u_scale;
@ -179,15 +179,13 @@ function init_webgl(state, context) {
}; };
context.buffers['sdf'] = { context.buffers['sdf'] = {
'b_packed_static': context.gl.createBuffer(), 'b_packed_static': gl.createBuffer(),
'b_packed_dynamic': context.gl.createBuffer(), 'b_packed_dynamic': gl.createBuffer(),
}; 'b_packed_static_index': gl.createBuffer(),
'b_packed_dynamic_index': gl.createBuffer(),
context.textures['sdf'] = {
'points': gl.createTexture(),
'indices': gl.createTexture()
}; };
context.textures['sdf'] = {};
context.textures['image'] = {}; context.textures['image'] = {};
const resize_canvas = (entries) => { const resize_canvas = (entries) => {

Loading…
Cancel
Save