Browse Source

BVH construction and modification

ssao
A.Olokhtonov 12 months ago
parent
commit
a991cf3247
  1. 1
      .gitignore
  2. 284
      client/bvh.js
  3. 26
      client/client_recv.js
  4. 130
      client/heapify.js
  5. 4
      client/index.html
  6. 20
      client/index.js
  7. 53
      client/math.js
  8. 48
      client/webgl_draw.js
  9. 9
      client/webgl_geometry.js
  10. 20
      client/webgl_listeners.js
  11. 69
      client/webgl_shaders.js
  12. 2
      client/websocket.js
  13. 20
      server/milton.js
  14. 4
      server/storage.js

1
.gitignore vendored

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
server/images
doca.txt
data/
client/*.dot

284
client/bvh.js

@ -0,0 +1,284 @@ @@ -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);
}

26
client/client_recv.js

@ -124,7 +124,7 @@ function bitmap_bbox(event) { @@ -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 @@ -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;
@ -175,12 +175,16 @@ function handle_event(state, context, event) { @@ -175,12 +175,16 @@ function handle_event(state, context, event) {
need_draw = true;
}
geometry_add_stroke(state, context, event, state.events.length);
event.index = state.events.length;
event.starting_index = state.starting_index;
state.stroke_count++;
if (event.points.length > 0) {
state.starting_index += (event.points.length - 1) * 4;
}
document.getElementById('debug-render-from').max = state.stroke_count;
document.getElementById('debug-render-to').max = state.stroke_count;
geometry_add_stroke(state, context, event, state.events.length, options.skip_bvh === true);
state.stroke_count++;
break;
}
@ -357,9 +361,17 @@ async function handle_message(state, context, d) { @@ -357,9 +361,17 @@ 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);
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;

130
client/heapify.js

@ -0,0 +1,130 @@ @@ -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(" ")}]`;
}
}

4
client/index.html

@ -10,6 +10,8 @@ @@ -10,6 +10,8 @@
<link rel="stylesheet" type="text/css" href="default.css?v=66">
<script type="text/javascript" src="aux.js?v=66"></script>
<script type="text/javascript" src="heapify.js?v=66"></script>
<script type="text/javascript" src="bvh.js?v=66"></script>
<script type="text/javascript" src="math.js?v=66"></script>
<script type="text/javascript" src="tools.js?v=66"></script>
<script type="text/javascript" src="webgl_geometry.js?v=66"></script>
@ -39,6 +41,8 @@ @@ -39,6 +41,8 @@
<label><input type="checkbox" id="debug-red">Simple shader</label>
<label><input type="checkbox" id="debug-do-prepass">Depth prepass</label>
<label><input type="checkbox" id="debug-force-clip-off">Force clipping off</label>
<label><input type="checkbox" id="debug-draw-bvh">Draw BVH</label>
<div class="flexcol">
<label><input type="checkbox" id="debug-limit-from">Limit events from</label>

20
client/index.js

@ -8,15 +8,15 @@ const config = { @@ -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 = { @@ -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() { @@ -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() { @@ -190,6 +198,8 @@ function main() {
'limit_to': false,
'render_from': 0,
'render_to': 0,
'force_clip_off': false,
'draw_bvh': false,
}
};

53
client/math.js

@ -182,25 +182,27 @@ function segment_interesects_quad(a, b, quad_topleft, quad_bottomright, quad_top @@ -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) { @@ -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) { @@ -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,14 +267,9 @@ function segments_onscreen(state, context, do_clip) { @@ -256,14 +267,9 @@ 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;
@ -278,7 +284,6 @@ function segments_onscreen(state, context, do_clip) { @@ -278,7 +284,6 @@ function segments_onscreen(state, context, do_clip) {
}
}
}
}
head += (event.points.length - 1) * 4;
}

48
client/webgl_draw.js

@ -57,7 +57,7 @@ function draw(state, context) { @@ -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) { @@ -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 = `
<span>Segments onscreen: ${do_clip ? index_count : '-' }</span>
<span>Canvas offset: (${state.canvas.offset.x}, ${state.canvas.offset.y})</span>
<span>Canvas zoom: ${Math.round(state.canvas.zoom * 100) / 100}</span>`;
<span>Canvas zoom: ${Math.round(state.canvas.zoom * 100000) / 100000}</span>`;
if (index_count > 0) {
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers['b_packed_static_index']);
@ -160,6 +160,46 @@ function draw(state, context) { @@ -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']);

9
client/webgl_geometry.js

@ -97,10 +97,11 @@ function geometry_prepare_stroke(state) { @@ -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) { @@ -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;
}

20
client/webgl_listeners.js

@ -26,23 +26,35 @@ function debug_panel_init(state, context) { @@ -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);
});

69
client/webgl_shaders.js

@ -1,3 +1,55 @@ @@ -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) { @@ -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) { @@ -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) { @@ -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(),

2
client/websocket.js

@ -28,7 +28,7 @@ async function ws_connect(state, context, first_connect = false) { @@ -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);

20
server/milton.js

@ -8,12 +8,11 @@ let first_point_y = null; @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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");

4
server/storage.js

@ -8,7 +8,7 @@ export const sessions = {}; @@ -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() { @@ -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)');

Loading…
Cancel
Save