diff --git a/client/config.js b/client/config.js index 9732f52..ed71c32 100644 --- a/client/config.js +++ b/client/config.js @@ -6,7 +6,8 @@ const config = { ws_reconnect_timeout: 2000, brush_preview_timeout: 1000, second_finger_timeout: 500, - animation_decay: 16, + // animation_decay: 16, + animation_decay: 10, vertical_zoom_speed_multiplier: 3, debug_print: false, draw_bvh: false, diff --git a/client/index.html b/client/index.html index d750018..ab01c9b 100644 --- a/client/index.html +++ b/client/index.html @@ -47,6 +47,7 @@ + @@ -151,7 +152,6 @@
-
diff --git a/client/webgl_draw.js b/client/webgl_draw.js index cf69519..cd79be5 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -235,7 +235,7 @@ function draw_strokes(state, width, height, programs, gl, lod_levels, segment_co tv_pop(batches_tv); } -async function draw(state, context, animate, ts) { +async function draw(state, context, animate, ts, external_draw = false) { const dt = ts - context.last_frame_ts; const cpu_before = performance.now(); @@ -631,7 +631,9 @@ async function draw(state, context, animate, ts) { if (state.canvas.target_zoom != state.canvas.zoom) { update_canvas_zoom(state, state.canvas.zoom, state.canvas.target_zoom, animate ? dt : 0); - schedule_draw(state, context, true); + if (!external_draw) { + schedule_draw(state, context, true); + } } } diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index d678164..5c02115 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -41,6 +41,178 @@ function debug_panel_init(state, context) { schedule_draw(state, context); }); + + document.getElementById('debug-render').addEventListener('click', async (e) => { + const encoded_chunks = []; + let total_chunk_bytes = 0; + + const handle_encoded_chunk = (chunk, metadata) => { + encoded_chunks.push(chunk); + total_chunk_bytes += chunk.byteLength; + }; + + const canvas = document.getElementById('c'); + + const init = { + output: handle_encoded_chunk, + error: (e) => { + console.log(e.message); + }, + }; + + const render_framerate = 240; + const render_frames = 720; + const render_output_framerate = 60; + const start_movement_frame = 240; + const start_movement2_frame = 480; + let updated_zoom = false; + + if (render_framerate % render_output_framerate !== 0) { + console.error('Render framerate MUST be divisible by output framerate'); + return; + } + + const render_ratio = render_framerate / render_output_framerate; + + if (render_frames % render_ratio !== 0) { + console.error('Render frames MUST be divisible render ratio'); + return; + } + + const render_output_frames = render_frames / render_ratio; + + const encoder_config = { + codec: "vp8", + width: canvas.width, + height: canvas.height, + displayWidth: canvas.width, + displayHeight: canvas.height, + bitrate: 100_000_000, + framerate: render_output_framerate, + }; + + const { supported } = await VideoEncoder.isConfigSupported(encoder_config); + if (supported) { + const encoder = new VideoEncoder(init); + encoder.configure(encoder_config); + + let zoom_level = 5; + + const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta); + state.canvas.zoom_screenp = {'x': canvas.width / 2, 'y': canvas.height / 2}; + state.canvas.zoom_level = zoom_level; + state.canvas.target_zoom = Math.pow(1.0 + dz, Math.abs(zoom_level)) + + const dst = new Uint8Array(canvas.width * canvas.height * 4); + const accum = new Uint16Array(canvas.width * canvas.height * 4); // double the size to fit many samples + const accum_final = new Uint8Array(canvas.width * canvas.height * 4); + + for (let i = 0; i < render_output_frames; ++i) { + const time_now = i / render_output_framerate; + + accum.fill(0); + + for (let subframe = 0; subframe < render_ratio; ++subframe) { + await draw(state, context, i > 0 || subframe > 0, (time_now + (subframe / render_ratio) / render_output_framerate) * 1000, true); + context.gl.readPixels(0, 0, canvas.width, canvas.height, context.gl.RGBA, context.gl.UNSIGNED_BYTE, dst); + + for (let j = 0; j < dst.byteLength; ++j) { + accum[j] += dst[j]; + } + + if (start_movement_frame <= i * render_ratio && i * render_ratio < start_movement2_frame) { + state.canvas.offset.x += (1000 - state.canvas.offset.x) * 0.05; + state.canvas.offset.y += (1000 - state.canvas.offset.y) * 0.05; + } + + if (i * render_ratio > start_movement2_frame) { + if (!updated_zoom) { + zoom_level = -25; + const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta); + state.canvas.zoom_level = zoom_level; + state.canvas.target_zoom = Math.pow(1.0 + dz, Math.abs(zoom_level)) + updated_zoom = true; + } + //state.canvas.offset.x += (3500 - state.canvas.offset.x) * 0.05; + //state.canvas.offset.y += (5000 - state.canvas.offset.y) * 0.05; + } + } + + for (let y = 0; y < canvas.height; ++y) { + for (let x = 0; x < canvas.width; ++x) { + for (let c = 0; c < 4; ++c) { + accum_final[(canvas.height - 1 - y) * canvas.width * 4 + x * 4 + c] = Math.round(accum[y * canvas.width * 4 + x * 4 + c] / render_ratio); + } + } + } + + const frame = new VideoFrame(accum_final, { timestamp: time_now, codedWidth: canvas.width, codedHeight: canvas.height, format: 'RGBA'}); + encoder.encode(frame, {keyFrame: true}); + frame.close(); + await encoder.flush(); + } + + const data = new Uint8Array(32 + render_frames * 12 + total_chunk_bytes); + + data[0] = 0x44; + data[1] = 0x4B; + data[2] = 0x49; + data[3] = 0x46; + + data[6] = 32; + data[8] = 0x56; + data[9] = 0x50; + data[10] = 0x38; + data[11] = 0x30; + + data[12] = encoder_config.width & 0xFF; + data[13] = (encoder_config.width >> 8) & 0xFF; + + data[14] = encoder_config.height & 0xFF; + data[15] = (encoder_config.height >> 8) & 0xFF; + + data[16] = render_output_framerate; // timebase denom + + data[20] = 1; // timebase numenator..? + + data[24] = render_output_frames; // frame count + + let offset = 32; + for (let i = 0; i < render_output_frames; ++i) { + const chunk = encoded_chunks[i]; + // frame header + // frame length + data[offset + 0] = chunk.byteLength & 0xFF; + data[offset + 1] = (chunk.byteLength >> 8) & 0xFF; + data[offset + 2] = (chunk.byteLength >> 16) & 0xFF; + data[offset + 3] = (chunk.byteLength >> 24) & 0xFF; + + // PTS + // set to frame number for now + data[offset + 4] = i & 0xFF; + data[offset + 5] = (i >> 8) & 0xFF; + data[offset + 6] = (i >> 16) & 0xFF; + data[offset + 7] = (i >> 24) & 0xFF; + + chunk.copyTo(new Uint8Array(data.buffer, offset + 12)); + + offset += 12 + chunk.byteLength; + } + + const blob = new Blob([data.buffer], { type: 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.style.display = 'none'; + a.download = 'rendered_stream.ivf'; // Set the suggested filename + document.body.appendChild(a); + a.click(); + } else { + console.error('Encoding config not supported'); + } + }); + document.getElementById('debug-begin-benchmark').addEventListener('click', (e) => { state.canvas.zoom_level = config.benchmark.zoom_level; state.canvas.offset.x = config.benchmark.offset.x;