From a991cf324703722b6ca23420a80dabef8513ddc6 Mon Sep 17 00:00:00 2001 From: "A.Olokhtonov" Date: Mon, 4 Dec 2023 22:52:59 +0300 Subject: [PATCH] BVH construction and modification --- .gitignore | 1 + client/bvh.js | 284 ++++++++++++++++++++++++++++++++++++++ client/client_recv.js | 28 ++-- client/heapify.js | 130 +++++++++++++++++ client/index.html | 4 + client/index.js | 20 ++- client/math.js | 73 +++++----- client/webgl_draw.js | 48 ++++++- client/webgl_geometry.js | 9 +- client/webgl_listeners.js | 20 ++- client/webgl_shaders.js | 69 ++++++++- client/websocket.js | 4 +- server/milton.js | 20 ++- server/storage.js | 4 +- 14 files changed, 640 insertions(+), 74 deletions(-) create mode 100644 client/bvh.js create mode 100644 client/heapify.js diff --git a/.gitignore b/.gitignore index afc01d3..9029ada 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ server/images doca.txt data/ +client/*.dot diff --git a/client/bvh.js b/client/bvh.js new file mode 100644 index 0000000..b1d43e4 --- /dev/null +++ b/client/bvh.js @@ -0,0 +1,284 @@ +// TODO: get rid of node_count +// +function bvh_make_leaf(bvh, index, stroke) { + const leaf = { + 'stroke_index': index, + 'bbox': stroke.bbox, + 'area': stroke.area, + 'parent_index': null, + 'is_leaf': true, + }; + + bvh.nodes.push(leaf); + + return bvh.nodes.length - 1; +} + +function bvh_make_internal(bvh) { + const node = { + 'child1': null, + 'child2': null, + 'parent_index': null, + 'is_leaf': false, + }; + + bvh.nodes.push(node); + + return bvh.nodes.length - 1; +} + +function bvh_compute_sah(bvh, new_leaf, potential_sibling, only_parent = false) { + let cost = 0; + let union_box; + + if (!only_parent) { + union_box = quad_union(new_leaf.bbox, potential_sibling.bbox); + + const internal_node_would_be = { 'bbox': union_box }; + const new_internal_node_cost = (union_box.x2 - union_box.x1) * (union_box.y2 - union_box.y1); + + cost += new_internal_node_cost; + } else { + union_box = new_leaf.bbox; + } + + let parent_index = potential_sibling.parent_index; + + while (parent_index !== null) { + const current_node = bvh.nodes[parent_index]; + const old_cost = current_node.area; + union_box = quad_union(current_node.bbox, union_box); + const new_cost = (union_box.x2 - union_box.x1) * (union_box.y2 - union_box.y1); + cost += new_cost - old_cost; + parent_index = current_node.parent_index; + } + + return cost; +} + +// todo area func + +function bvh_find_best_sibling(bvh, leaf_index) { + // branch and bound + + const leaf = bvh.nodes[leaf_index]; + const leaf_cost = (leaf.bbox.x2 - leaf.bbox.x1) * (leaf.bbox.y2 - leaf.bbox.y1); + + let best_cost = bvh_compute_sah(bvh, leaf, bvh.nodes[bvh.root]); + let best_index = bvh.root; + + bvh.pqueue.clear(); + bvh.pqueue.push(best_index, best_cost); + + while (bvh.pqueue.size > 0) { + const current_index = bvh.pqueue.pop(); + const current_node = bvh.nodes[current_index]; + const cost = bvh_compute_sah(bvh, current_node, leaf); + + if (cost < best_cost) { + best_cost = cost; + best_index = current_index; + } + + if (!current_node.is_leaf) { + const child1 = bvh.nodes[current_node.child1]; + const lower_bound_for_children = bvh_compute_sah(bvh, child1, leaf, true) + leaf_cost; + if (lower_bound_for_children < best_cost) { + bvh.pqueue.push(current_node.child1, lower_bound_for_children); + bvh.pqueue.push(current_node.child2, lower_bound_for_children); + } + } + } + + return best_index; +} + +function bvh_rotate(bvh, index) { + +} + +function bvh_add_stroke(bvh, index, stroke) { + const leaf_index = bvh_make_leaf(bvh, index, stroke); + + if (bvh.node_count === 0) { + bvh.root = leaf_index; + bvh.node_count++; + return; + } + + bvh.node_count++; + + if (bvh.pqueue.capacity < Math.ceil(bvh.node_count * 1.2)) { + bvh.pqueue = new MinQueue(bvh.pqueue.capacity * 2); + } + + // It's as easy as 1-2-3 + + // 1. Find best sibling for leaf + const sibling = bvh_find_best_sibling(bvh, leaf_index); + + // 2. Create new parent + const old_parent = bvh.nodes[sibling].parent_index; + const new_parent = bvh_make_internal(bvh); + + bvh.nodes[new_parent].parent_index = old_parent; + bvh.nodes[new_parent].bbox = quad_union(stroke.bbox, bvh.nodes[sibling].bbox); + + if (old_parent !== null) { + // The sibling was not the root + if (bvh.nodes[old_parent].child1 === sibling) { + bvh.nodes[old_parent].child1 = new_parent; + } else { + bvh.nodes[old_parent].child2 = new_parent; + } + + bvh.nodes[new_parent].child1 = sibling; + bvh.nodes[new_parent].child2 = leaf_index; + + bvh.nodes[sibling].parent_index = new_parent; + bvh.nodes[leaf_index].parent_index = new_parent; + } else { + // The sibling was the root + bvh.nodes[new_parent].child1 = sibling; + bvh.nodes[new_parent].child2 = leaf_index; + + bvh.nodes[sibling].parent_index = new_parent; + bvh.nodes[leaf_index].parent_index = new_parent; + + bvh.root = new_parent; + } + + const new_bbox = quad_union(bvh.nodes[bvh.nodes[new_parent].child1].bbox, bvh.nodes[bvh.nodes[new_parent].child2].bbox); + bvh.nodes[new_parent].bbox = new_bbox; + bvh.nodes[new_parent].area = (new_bbox.x2 - new_bbox.x1) * (new_bbox.y2 - new_bbox.y1); + + bvh.node_count++; + + // 3. Refit and rotate + let refit_index = bvh.nodes[leaf_index].parent_index; + while (refit_index !== null) { + const child1 = bvh.nodes[refit_index].child1; + const child2 = bvh.nodes[refit_index].child2; + + bvh.nodes[refit_index].bbox = quad_union(bvh.nodes[child1].bbox, bvh.nodes[child2].bbox); + + bvh_rotate(bvh, refit_index); + + refit_index = bvh.nodes[refit_index].parent_index; + } +} + +function bvh_intersect_quad(bvh, quad) { + if (bvh.root === null) { + return []; + } + + const stack = [bvh.root]; + const result = []; + + while (stack.length > 0) { + const node_index = stack.pop(); + const node = bvh.nodes[node_index]; + + if (!quads_intersect(node.bbox, quad)) { + continue; + } + + if (node.is_leaf) { + result.push(node.stroke_index); + } else { + stack.push(node.child1, node.child2); + } + } + + return result; +} + +function bvh_clip(state, context) { + if (state.onscreen_segments === null) { + let total_points = 0; + + for (const event of state.events) { + if (event.type === EVENT.STROKE && !event.deleted && event.points.length > 0) { + total_points += event.points.length - 1; + } + } + + if (total_points > 0) { + state.onscreen_segments = new Uint32Array(total_points * 6); + } + } + + let at = 0; + + 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}; + + const stroke_indices = bvh_intersect_quad(state.bvh, screen); + stroke_indices.sort(); + + for (const i of stroke_indices) { + if (state.debug.limit_to && i >= state.debug.render_to) break; + + const event = state.events[i]; + + if (!(state.debug.limit_from && i < state.debug.render_from)) { + if (event.type === EVENT.STROKE && !event.deleted && event.points.length > 0) { + for (let j = 0; j < event.points.length - 1; ++j) { + let base = event.starting_index + j * 4; + // We draw quads as [1, 2, 3, 4, 3, 2] + state.onscreen_segments[at + 0] = base + 0; + state.onscreen_segments[at + 1] = base + 1; + state.onscreen_segments[at + 2] = base + 2; + state.onscreen_segments[at + 3] = base + 3; + state.onscreen_segments[at + 4] = base + 2; + state.onscreen_segments[at + 5] = base + 1; + + at += 6; + } + } + } + } + + return at; + +} + +function bvh_construct_rec(bvh, vertical, strokes) { + if (strokes.length > 1) { + // internal + let sorted_strokes; + + if (vertical) { + sorted_strokes = strokes.toSorted((a, b) => a.bbox.cy - b.bbox.cy); + } else { + sorted_strokes = strokes.toSorted((a, b) => a.bbox.cx - b.bbox.cx); + } + + const node_index = bvh_make_internal(bvh); + const left_of_split_count = Math.floor(strokes.length / 2); + + const child1 = bvh_construct_rec(bvh, !vertical, sorted_strokes.slice(0, left_of_split_count)); + const child2 = bvh_construct_rec(bvh, !vertical, sorted_strokes.slice(left_of_split_count, sorted_strokes.length)); + + bvh.nodes[child1].parent_index = node_index; + bvh.nodes[child2].parent_index = node_index; + + bvh.nodes[node_index].child1 = child1; + bvh.nodes[node_index].child2 = child2; + bvh.nodes[node_index].bbox = quad_union(bvh.nodes[child1].bbox, bvh.nodes[child2].bbox); + + return node_index; + } else { + // leaf + return bvh_make_leaf(bvh, strokes[0].index, strokes[0]); + } +} + +function bvh_construct(state) { + state.bvh.root = bvh_construct_rec(state.bvh, true, state.events); +} diff --git a/client/client_recv.js b/client/client_recv.js index 74d4f38..2bf76b1 100644 --- a/client/client_recv.js +++ b/client/client_recv.js @@ -124,7 +124,7 @@ function bitmap_bbox(event) { 'xmin': event.x, 'xmax': event.x + event.bitmap.width, 'ymin': event.y, - 'ymax': event.y + event.bitmap.height + 'ymax': event.y + event.bitmap.height, }; return bbox; @@ -138,7 +138,7 @@ function init_player_defaults(state, player_id, color = config.default_color, wi }; } -function handle_event(state, context, event) { +function handle_event(state, context, event, options = {}) { if (config.debug_print) console.debug(`event type ${event.type} from user ${event.user_id}`); let need_draw = false; @@ -174,13 +174,17 @@ function handle_event(state, context, event) { geometry_clear_player(state, context, event.user_id); need_draw = true; } + + event.index = state.events.length; + event.starting_index = state.starting_index; - geometry_add_stroke(state, context, event, state.events.length); + if (event.points.length > 0) { + state.starting_index += (event.points.length - 1) * 4; + } - state.stroke_count++; + geometry_add_stroke(state, context, event, state.events.length, options.skip_bvh === true); - document.getElementById('debug-render-from').max = state.stroke_count; - document.getElementById('debug-render-to').max = state.stroke_count; + state.stroke_count++; break; } @@ -357,10 +361,18 @@ async function handle_message(state, context, d) { for (let i = 0; i < event_count; ++i) { const event = des_event(d); - handle_event(state, context, event); - state.events.push(event); + handle_event(state, context, event, {'skip_bvh': true}); + + if (event.type !== EVENT.STROKE || event.points.length > 0) { + state.events.push(event); + } } + bvh_construct(state); + + document.getElementById('debug-render-from').max = state.stroke_count; + document.getElementById('debug-render-to').max = state.stroke_count; + do_draw = true; send_ack(event_count); diff --git a/client/heapify.js b/client/heapify.js new file mode 100644 index 0000000..6b15936 --- /dev/null +++ b/client/heapify.js @@ -0,0 +1,130 @@ +// translated by esbuild to js. original typescript source MIT licensed at https://github.com/luciopaiva/heapify + +const ROOT_INDEX = 1; +class MinQueue { + constructor(capacity = 64, keys = [], priorities = [], KeysBackingArrayType = Uint32Array, PrioritiesBackingArrayType = Uint32Array) { + this._capacity = capacity; + this._keys = new KeysBackingArrayType(capacity + ROOT_INDEX); + this._priorities = new PrioritiesBackingArrayType(capacity + ROOT_INDEX); + this._hasPoppedElement = false; + if (keys.length !== priorities.length) { + throw new Error("Number of keys does not match number of priorities provided."); + } + if (capacity < keys.length) { + throw new Error("Capacity less than number of provided keys."); + } + for (let i = 0; i < keys.length; i++) { + this._keys[i + ROOT_INDEX] = keys[i]; + this._priorities[i + ROOT_INDEX] = priorities[i]; + } + this.length = keys.length; + for (let i = keys.length >>> 1; i >= ROOT_INDEX; i--) { + this.bubbleDown(i); + } + } + get capacity() { + return this._capacity; + } + clear() { + this.length = 0; + this._hasPoppedElement = false; + } + bubbleUp(index) { + const key = this._keys[index]; + const priority = this._priorities[index]; + while (index > ROOT_INDEX) { + const parentIndex = index >>> 1; + if (this._priorities[parentIndex] <= priority) { + break; + } + this._keys[index] = this._keys[parentIndex]; + this._priorities[index] = this._priorities[parentIndex]; + index = parentIndex; + } + this._keys[index] = key; + this._priorities[index] = priority; + } + bubbleDown(index) { + const key = this._keys[index]; + const priority = this._priorities[index]; + const halfLength = ROOT_INDEX + (this.length >>> 1); + const lastIndex = this.length + ROOT_INDEX; + while (index < halfLength) { + const left = index << 1; + let childPriority = this._priorities[left]; + let childKey = this._keys[left]; + let childIndex = left; + const right = left + 1; + if (right < lastIndex) { + const rightPriority = this._priorities[right]; + if (rightPriority < childPriority) { + childPriority = rightPriority; + childKey = this._keys[right]; + childIndex = right; + } + } + if (childPriority >= priority) { + break; + } + this._keys[index] = childKey; + this._priorities[index] = childPriority; + index = childIndex; + } + this._keys[index] = key; + this._priorities[index] = priority; + } + push(key, priority) { + if (this.length === this._capacity) { + throw new Error("Heap has reached capacity, can't push new items"); + } + if (this._hasPoppedElement) { + this._keys[ROOT_INDEX] = key; + this._priorities[ROOT_INDEX] = priority; + this.length++; + this.bubbleDown(ROOT_INDEX); + this._hasPoppedElement = false; + } else { + const pos = this.length + ROOT_INDEX; + this._keys[pos] = key; + this._priorities[pos] = priority; + this.length++; + this.bubbleUp(pos); + } + } + pop() { + if (this.length === 0) { + return void 0; + } + this.removePoppedElement(); + this.length--; + this._hasPoppedElement = true; + return this._keys[ROOT_INDEX]; + } + peekPriority() { + this.removePoppedElement(); + return this._priorities[ROOT_INDEX]; + } + peek() { + this.removePoppedElement(); + return this._keys[ROOT_INDEX]; + } + removePoppedElement() { + if (this._hasPoppedElement) { + this._keys[ROOT_INDEX] = this._keys[this.length + ROOT_INDEX]; + this._priorities[ROOT_INDEX] = this._priorities[this.length + ROOT_INDEX]; + this.bubbleDown(ROOT_INDEX); + this._hasPoppedElement = false; + } + } + get size() { + return this.length; + } + dumpRawPriorities() { + this.removePoppedElement(); + const result = Array(this.length - ROOT_INDEX); + for (let i = 0; i < this.length; i++) { + result[i] = this._priorities[i + ROOT_INDEX]; + } + return `[${result.join(" ")}]`; + } +} diff --git a/client/index.html b/client/index.html index 369fe7f..97c8c3f 100644 --- a/client/index.html +++ b/client/index.html @@ -10,6 +10,8 @@ + + @@ -39,6 +41,8 @@ + +
diff --git a/client/index.js b/client/index.js index 250869f..317936b 100644 --- a/client/index.js +++ b/client/index.js @@ -8,15 +8,15 @@ const config = { // ws_url: 'wss://desk.some.website/ws/', // ping_url: 'https://desk.some.website/api/ping', // image_url: 'https://desk.some.website/images/', - ws_url: 'wss://localhost/ws/', - ping_url: 'https://localhost/api/ping', - image_url: 'https://localhost/images/', + ws_url: `wss://${window.location.host}/ws/`, + ping_url: `https://${window.location.host}/api/ping`, + image_url: `https://${window.location.host}/images/`, sync_timeout: 1000, ws_reconnect_timeout: 2000, brush_preview_timeout: 1000, second_finger_timeout: 500, buffer_first_touchmoves: 5, - debug_print: true, + debug_print: false, min_zoom: 0.0000005, max_zoom: 10.0, initial_offline_timeout: 1000, @@ -27,7 +27,7 @@ const config = { initial_dynamic_bytes: 4096, frametime_window_size: 100, tile_size: 16, - clip_zoom_threshold: 0.1, + clip_zoom_threshold: 0.00003, }; const EVENT = Object.freeze({ @@ -164,6 +164,14 @@ function main() { 'queue': [], 'events': [], 'stroke_count': 0, + 'starting_index': 0, + + 'bvh': { + 'nodes': [], + 'node_count': 0, + 'root': null, + 'pqueue': new MinQueue(1024), + }, 'tools': { 'active': null, @@ -190,6 +198,8 @@ function main() { 'limit_to': false, 'render_from': 0, 'render_to': 0, + 'force_clip_off': false, + 'draw_bvh': false, } }; diff --git a/client/math.js b/client/math.js index ef82c92..5d108e5 100644 --- a/client/math.js +++ b/client/math.js @@ -182,25 +182,27 @@ function segment_interesects_quad(a, b, quad_topleft, quad_bottomright, quad_top 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); +function stroke_bbox(stroke) { + const radius = stroke.width / 2; + + let min_x = stroke.points[0].x - radius; + let max_x = stroke.points[0].x + radius; + + let min_y = stroke.points[0].y - radius; + let max_y = stroke.points[0].y + radius; + + for (const p of stroke.points) { + min_x = Math.min(min_x, p.x - radius); + min_y = Math.min(min_y, p.y - radius); + max_x = Math.max(max_x, p.x + radius); + max_y = Math.max(max_y, p.y + radius); } - return {'x1': min_x, 'y1': min_y, 'x2': max_x, 'y2': max_y}; + return {'x1': min_x, 'y1': min_y, 'x2': max_x, 'y2': max_y, 'cx': (max_x + min_x) / 2, 'cy': (max_y + min_y) / 2}; } -function quad_onscreen(screen, bbox) { - if (screen.x1 < bbox.x2 && screen.x2 > bbox.x1 && screen.y2 > bbox.y1 && screen.y1 < bbox.y2) { +function quads_intersect(a, b) { + if (a.x1 < b.x2 && a.x2 > b.x1 && a.y2 > b.y1 && a.y1 < b.y2) { return true; } @@ -215,6 +217,15 @@ function quad_fully_onscreen(screen, bbox) { return false; } +function quad_union(a, b) { + return { + 'x1': Math.min(a.x1, b.x1), + 'y1': Math.min(a.y1, b.y1), + 'x2': Math.max(a.x2, b.x2), + 'y2': Math.max(a.y2, b.y2), + }; +} + function segments_onscreen(state, context, do_clip) { // TODO: handle stroke width @@ -222,7 +233,7 @@ function segments_onscreen(state, context, do_clip) { let total_points = 0; for (const event of state.events) { - if (event.type === EVENT.STROKE && !event.deleted) { + if (event.type === EVENT.STROKE && !event.deleted && event.points.length > 0) { total_points += event.points.length - 1; } } @@ -256,25 +267,19 @@ function segments_onscreen(state, context, do_clip) { const event = state.events[i]; if (!(state.debug.limit_from && i < state.debug.render_from)) { - if (event.type === EVENT.STROKE && !event.deleted) { - if (!do_clip || quad_onscreen(screen, event.bbox)) { - const fully_onscreen = !do_clip || quad_fully_onscreen(screen, event.bbox); + if (event.type === EVENT.STROKE && !event.deleted && event.points.length > 0) { + if (!do_clip || quads_intersect(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 (fully_onscreen || 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] - state.onscreen_segments[at + 0] = base + 0; - state.onscreen_segments[at + 1] = base + 1; - state.onscreen_segments[at + 2] = base + 2; - state.onscreen_segments[at + 3] = base + 3; - state.onscreen_segments[at + 4] = base + 2; - state.onscreen_segments[at + 5] = base + 1; - - at += 6; - } + let base = head + j * 4; + // We draw quads as [1, 2, 3, 4, 3, 2] + state.onscreen_segments[at + 0] = base + 0; + state.onscreen_segments[at + 1] = base + 1; + state.onscreen_segments[at + 2] = base + 2; + state.onscreen_segments[at + 3] = base + 3; + state.onscreen_segments[at + 4] = base + 2; + state.onscreen_segments[at + 5] = base + 1; + + at += 6; } } } diff --git a/client/webgl_draw.js b/client/webgl_draw.js index 9679194..56183ce 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -57,7 +57,7 @@ function draw(state, context) { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); let index_count; - const do_clip = false; //(state.canvas.zoom > config.clip_zoom_threshold); + const do_clip = !state.debug.force_clip_off; //(state.canvas.zoom > config.clip_zoom_threshold); if (do_clip) { context.need_index_upload = true; @@ -65,19 +65,19 @@ function draw(state, context) { if (do_clip || context.need_index_upload) { const before_clip = performance.now(); - index_count = segments_onscreen(state, context, do_clip); + index_count = bvh_clip(state, context); const after_clip = performance.now(); } if (!do_clip && !context.need_index_upload) { index_count = context.full_index_count; } - //console.debug('clip', after_clip - before_clip); + document.getElementById('debug-stats').innerHTML = ` Segments onscreen: ${do_clip ? index_count : '-' } Canvas offset: (${state.canvas.offset.x}, ${state.canvas.offset.y}) - Canvas zoom: ${Math.round(state.canvas.zoom * 100) / 100}`; + Canvas zoom: ${Math.round(state.canvas.zoom * 100000) / 100000}`; if (index_count > 0) { gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers['b_packed_static_index']); @@ -160,6 +160,46 @@ function draw(state, context) { } } + if (state.debug.draw_bvh) { + const points = new Float32Array(state.bvh.nodes.length * 6 * 2); + + for (let i = 0; i < state.bvh.nodes.length; ++i) { + const box = state.bvh.nodes[i].bbox; + + points[i * 12 + 0] = box.x1; + points[i * 12 + 1] = box.y1; + points[i * 12 + 2] = box.x2; + points[i * 12 + 3] = box.y1; + points[i * 12 + 4] = box.x1; + points[i * 12 + 5] = box.y2; + + points[i * 12 + 6] = box.x2; + points[i * 12 + 7] = box.y2; + points[i * 12 + 8] = box.x1; + points[i * 12 + 9] = box.y2; + points[i * 12 + 10] = box.x2; + points[i * 12 + 11] = box.y1; + } + + locations = context.locations['debug']; + buffers = context.buffers['debug']; + + gl.useProgram(context.programs['debug']); + gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']); + + gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height); + gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); + gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); + + gl.enableVertexAttribArray(locations['a_pos']); + gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 8, 0); + + gl.clear(gl.DEPTH_BUFFER_BIT); + + gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW); + gl.drawArrays(gl.TRIANGLES, 0, points.length / 2); + } + /* if (dynamic_points > 0) { gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed_dynamic']); diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index 94f7157..33fd2dc 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -97,10 +97,11 @@ function geometry_prepare_stroke(state) { }; } -function geometry_add_stroke(state, context, stroke, stroke_index) { - if (!state.online || !stroke) return; +function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh) { + if (!state.online || !stroke || stroke.points.length === 0) return; - stroke.bbox = stroke_bbox(stroke.points); + stroke.bbox = stroke_bbox(stroke); + stroke.area = (stroke.bbox.x2 - stroke.bbox.x1) * (stroke.bbox.y2 - stroke.bbox.y1); let bytes_left = context.static_serializer.size - context.static_serializer.offset; let bytes_needed = stroke.points.length * 4 * config.bytes_per_point; @@ -112,6 +113,8 @@ function geometry_add_stroke(state, context, stroke, stroke_index) { } push_stroke(context.static_serializer, stroke, stroke_index); + if (!skip_bvh) bvh_add_stroke(state.bvh, stroke_index, stroke); + context.need_static_upload = true; } diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index 00b0822..3d23714 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -26,23 +26,35 @@ function debug_panel_init(state, context) { document.getElementById('debug-do-prepass').checked = state.debug.do_prepass; document.getElementById('debug-limit-from').checked = state.debug.limit_from; document.getElementById('debug-limit-to').checked = state.debug.limit_to; + document.getElementById('debug-force-clip-off').checked = state.debug.force_clip_off; + document.getElementById('debug-draw-bvh').checked = state.debug.draw_bvh; - document.getElementById('debug-red').addEventListener('click', (e) => { + document.getElementById('debug-draw-bvh').addEventListener('change', (e) => { + state.debug.draw_bvh = e.target.checked; + schedule_draw(state, context); + }); + + document.getElementById('debug-force-clip-off').addEventListener('change', (e) => { + state.debug.force_clip_off = e.target.checked; + schedule_draw(state, context); + }); + + document.getElementById('debug-red').addEventListener('change', (e) => { state.debug.red = e.target.checked; schedule_draw(state, context); }); - document.getElementById('debug-do-prepass').addEventListener('click', (e) => { + document.getElementById('debug-do-prepass').addEventListener('change', (e) => { state.debug.do_prepass = e.target.checked; schedule_draw(state, context); }); - document.getElementById('debug-limit-from').addEventListener('click', (e) => { + document.getElementById('debug-limit-from').addEventListener('change', (e) => { state.debug.limit_from = e.target.checked; schedule_draw(state, context); }); - document.getElementById('debug-limit-to').addEventListener('click', (e) => { + document.getElementById('debug-limit-to').addEventListener('change', (e) => { state.debug.limit_to = e.target.checked; schedule_draw(state, context); }); diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index bac05b2..6d8b696 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -1,3 +1,55 @@ +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; + 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; @@ -191,7 +243,6 @@ function init_webgl(state, context) { gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); - //gl.blendFunc(gl.SRC_ALPHA, gl.DST_ALPHA); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.GEQUAL); @@ -210,12 +261,24 @@ function init_webgl(state, context) { 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'), @@ -243,6 +306,10 @@ function init_webgl(state, context) { } }; + context.buffers['debug'] = { + 'b_packed': gl.createBuffer(), + }; + context.buffers['sdf'] = { 'b_packed_static': gl.createBuffer(), 'b_packed_dynamic': gl.createBuffer(), diff --git a/client/websocket.js b/client/websocket.js index 01c50ac..2b646e6 100644 --- a/client/websocket.js +++ b/client/websocket.js @@ -28,7 +28,7 @@ async function ws_connect(state, context, first_connect = false) { } } } catch (e) { - // console.log('Could not ping the server:', e); + console.log('Could not ping the server:', e); } state.timers.offline_toast = setTimeout(() => ws_connect(state, context, first_connect), config.ws_reconnect_timeout); @@ -72,4 +72,4 @@ function on_close(state, context) { function on_error(state, context) { ws.close(state, context); -} \ No newline at end of file +} diff --git a/server/milton.js b/server/milton.js index 6b1e6b3..bdb442a 100644 --- a/server/milton.js +++ b/server/milton.js @@ -8,12 +8,11 @@ let first_point_y = null; function parse_and_insert_stroke(desk_id, line) { const stroke_id = math.fast_random32(); - const points = new Float32Array(line.split(' ').filter(s => s.length > 0).map(i => parseFloat(i))); + const words = line.split(' '); + const width = parseInt(words.shift()); + const points = new Float32Array(words.map(i => parseFloat(i))); if (first_point_x === null) { - first_point_x = 1; - first_point_y = 1; - } else if (first_point_x === 1) { first_point_x = points[0]; first_point_y = points[1]; } @@ -23,9 +22,8 @@ function parse_and_insert_stroke(desk_id, line) { points[i + 1] -= first_point_y; } - storage.queries.insert_stroke.run({ - '$id': stroke_id, - '$width': 8, // possibly handle pressure here if we ever support it + const stroke_res = storage.queries.insert_stroke.get({ + '$width': width, '$color': 0, '$points': points }); @@ -34,7 +32,7 @@ function parse_and_insert_stroke(desk_id, line) { '$type': EVENT.STROKE, '$desk_id': desk_id, '$session_id': 0, - '$stroke_id': stroke_id, + '$stroke_id': stroke_res.id, '$image_id': 0, '$x': 0, '$y': 0, @@ -44,7 +42,7 @@ function parse_and_insert_stroke(desk_id, line) { async function import_milton_file_to_sqlite(fullpath) { storage.startup(); - const desk_id = 542; // math.fast_random32(); + const desk_id = 9881; // math.fast_random32(); console.log(`Importing ${fullpath} into desk ${desk_id}`); @@ -62,7 +60,7 @@ async function import_milton_file_to_sqlite(fullpath) { parse_and_insert_stroke(desk_id, input_lines[i]); } - consoe.log(`Finished importing desk ${desk_id}`); + console.log(`Finished importing desk ${desk_id}`); } -import_milton_file_to_sqlite("/home/aolo2/desk2/server/points.txt"); +import_milton_file_to_sqlite("/code/desk2/server/points.txt"); diff --git a/server/storage.js b/server/storage.js index 23fa0da..ba9c274 100644 --- a/server/storage.js +++ b/server/storage.js @@ -8,7 +8,7 @@ export const sessions = {}; export const desks = {}; export const queries = {}; -let db = null; +export let db = null; export function startup() { const path = `${config.DATADIR}/db.sqlite`; @@ -69,7 +69,7 @@ export function startup() { // INSERT queries.insert_desk = db.query('INSERT INTO desks (id, title, sn) VALUES ($id, $title, 0)'); - queries.insert_stroke = db.query('INSERT INTO strokes (id, width, color, points) VALUES ($id, $width, $color, $points)'); + queries.insert_stroke = db.query('INSERT INTO strokes (id, width, color, points) VALUES ($id, $width, $color, $points) RETURNING id'); queries.insert_session = db.query('INSERT INTO sessions (id, desk_id, lsn) VALUES ($id, $desk_id, 0)'); queries.insert_event = db.query('INSERT INTO events (type, desk_id, session_id, stroke_id, image_id, x, y) VALUES ($type, $desk_id, $session_id, $stroke_id, $image_id, $x, $y)');