Compare commits

..

12 Commits

Author SHA1 Message Date
Aaron Fenyes
f148552964 Ray-caster: only draw the top few fragments
This seems to increase graphics pipe use from 50--60% to 55--65%.
2024-08-29 15:01:38 -07:00
Aaron Fenyes
6db9f5be6c Enlarge the test construction to six spheres
In debug mode, assign most of the color scale to even layer counts. Odd
layer counts are topologically prohibited, so we should rarely see bugs
severe enough to produce them.
2024-08-28 17:14:55 -07:00
Aaron Fenyes
3a721a4cc8 Ray-caster: enlarge the construction data uniforms
No noticeable effect on GPU performance.
2024-08-28 16:06:33 -07:00
Aaron Fenyes
8fde202911 Ray-caster: drop unused depth-pair array
This doesn't affect GPU performance noticeably, so benchmarks before and
after the change should be comparable.
2024-08-28 15:52:19 -07:00
Aaron Fenyes
4afc82034b Ray-caster: sort fragments while shading 2024-08-28 14:37:29 -07:00
Aaron Fenyes
e80adf831d Ray-caster: correct hit detection 2024-08-28 01:59:46 -07:00
Aaron Fenyes
3d7ee98dd6 Ray-caster: check layer count
In debug mode, show the layer count instead of the shaded image. This
reveals a bug: testing whether the hit depth is NaN doesn't actually
detect sphere misses, because `sphere_cast` returns -1 rather than NaN
to indicate a miss.
2024-08-28 01:55:46 -07:00
Aaron Fenyes
c04e29f586 Reduce the frame interval sample frequency
At the higher frequency we were using earlier, the overhead from
updating the readout contributed significantly to the frame interval.
2024-08-28 01:47:38 -07:00
Aaron Fenyes
01c2af6615 Rotate and translate construction
In the process, write code to make updates that depend on the time
between frames.
2024-08-27 18:39:58 -07:00
Aaron Fenyes
a40a110788 Ray-caster: only draw when the scene is changed
This is how I typically schedule draw calls in JavaScript applications.
The baseline CPU activity for the display prototype is now in line with
other pages (though perhaps a bit higher), and the profiler shows little
time being spent in draw calls, even when I'm continually moving a
slider. The interface feels pretty responsive overall, although the
sliders seem to be lagging a bit.
2024-08-27 13:55:08 -07:00
Aaron Fenyes
f62f44b5a7 Optimization: decouple internal and uniform SPHERE_MAX 2024-08-27 00:00:48 -07:00
Aaron Fenyes
ec48592ef1 Ray-caster: add a frame time monitor
It's time to start optimizing. Frame time is easy to measure, and we can
use it to gauge responsiveness.
2024-08-26 23:39:51 -07:00
3 changed files with 202 additions and 80 deletions

View File

@ -22,6 +22,7 @@ console_error_panic_hook = { version = "0.1.7", optional = true }
version = "0.3.69"
features = [
'HtmlCanvasElement',
'Performance',
'WebGl2RenderingContext',
'WebGlBuffer',
'WebGlProgram',

View File

@ -25,7 +25,7 @@ vecInv sphere(vec3 center, float radius) {
// construction. the SPHERE_MAX array size seems to affect frame rate a lot,
// even though we should only be using the first few elements of each array
const int SPHERE_MAX = 12;
const int SPHERE_MAX = 200;
uniform int sphere_cnt;
uniform vecInv sphere_list[SPHERE_MAX];
uniform vec3 color_list[SPHERE_MAX];
@ -40,7 +40,7 @@ uniform vec2 radius;
uniform float opacity;
uniform float highlight;
uniform int layer_threshold;
uniform bool test_mode;
uniform bool debug_mode;
// light and camera
const float focal_slope = 0.3;
@ -135,36 +135,63 @@ void main() {
vec3 dir = vec3(focal_slope * scr, -1.);
// cast rays through the spheres
vec2 depth_pairs [SPHERE_MAX];
taggedFrag frags [2*SPHERE_MAX];
int frag_cnt = 0;
for (int i = 0; i < sphere_cnt; ++i) {
vec2 hit_depths = sphere_cast(sphere_list[i], dir);
if (!isnan(hit_depths[0])) {
frags[frag_cnt] = sphere_shading(sphere_list[i], hit_depths[0] * dir, color_list[i], i);
++frag_cnt;
const int LAYER_MAX = 12;
taggedFrag frags [LAYER_MAX];
int layer_cnt = 0;
for (int id = 0; id < sphere_cnt; ++id) {
// find out where the ray hits the sphere
vec2 hit_depths = sphere_cast(sphere_list[id], dir);
// insertion-sort the fragments we hit into the fragment list
for (int side = 0; side < 2; ++side) {
float hit_z = -hit_depths[side];
if (0. > hit_z) {
for (int layer = layer_cnt; layer >= 0; --layer) {
if (layer < 1 || frags[layer-1].pt.z >= hit_z) {
// we're not as close to the screen as the fragment
// before the empty slot, so insert here
if (layer < LAYER_MAX) {
frags[layer] = sphere_shading(
sphere_list[id],
hit_depths[side] * dir,
color_list[id],
id
);
}
break;
} else {
// we're closer to the screen than the fragment before
// the empty slot, so move that fragment into the empty
// slot
frags[layer] = frags[layer-1];
}
}
layer_cnt = min(layer_cnt + 1, LAYER_MAX);
}
if (!isnan(hit_depths[1])) {
frags[frag_cnt] = sphere_shading(sphere_list[i], hit_depths[1] * dir, color_list[i], i);
++frag_cnt;
}
}
// sort the fragments by depth, using an insertion sort
for (int take = 1; take < frag_cnt; ++take) {
taggedFrag pulled = frags[take];
for (int put = take; put >= 0; --put) {
if (put < 1 || frags[put-1].pt.z >= pulled.pt.z) {
frags[put] = pulled;
break;
/* DEBUG */
// in debug mode, show the layer count instead of the shaded image
if (debug_mode) {
// at the bottom of the screen, show the color scale instead of the
// layer count
if (gl_FragCoord.y < 10.) layer_cnt = 2 * int(8. * gl_FragCoord.x / resolution.x);
// convert number to color
vec3 color;
if (layer_cnt % 2 == 0) {
ivec3 bits = layer_cnt / ivec3(2, 4, 8);
color = mod(vec3(bits), 2.);
} else {
frags[put] = frags[put-1];
}
color = vec3(0.5);
}
outColor = vec4(color, 1.);
return;
}
// highlight intersections and cusps
for (int i = frag_cnt-1; i >= 1; --i) {
for (int i = layer_cnt-1; i >= 1; --i) {
// intersections
taggedFrag frag0 = frags[i];
taggedFrag frag1 = frags[i-1];
@ -187,7 +214,7 @@ void main() {
// composite the sphere fragments
vec3 color = vec3(0.);
for (int i = frag_cnt-1; i >= layer_threshold; --i) {
for (int i = layer_cnt-1; i >= layer_threshold; --i) {
if (frags[i].pt.z < 0.) {
vec4 frag_color = frags[i].color;
color = mix(color, frag_color.rgb, frag_color.a);

View File

@ -9,9 +9,9 @@
extern crate js_sys;
use core::array;
use nalgebra::DVector;
use nalgebra::{DMatrix, DVector};
use sycamore::{prelude::*, motion::create_raf, rt::{JsCast, JsValue}};
use web_sys::{console, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation};
use web_sys::{console, window, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation};
mod engine;
@ -96,22 +96,58 @@ fn main() {
let radius_y = create_signal(1.0);
let opacity = create_signal(0.5);
let highlight = create_signal(0.2);
let layer_threshold = create_signal(0.0);
let debug_mode = create_signal(false);
let turntable = create_signal(false);
let layer_threshold = create_signal(0.0); /* DEBUG */
let debug_mode = create_signal(false); /* DEBUG */
/* INSTRUMENTS */
const SAMPLE_PERIOD: i32 = 60;
let mut last_sample_time = 0.0;
let mut frames_since_last_sample = 0;
let mean_frame_interval = create_signal(0.0);
// display
let display = create_node_ref();
// change listener
let scene_changed = create_signal(true);
create_effect(move || {
ctrl_x.track();
ctrl_y.track();
radius_x.track();
radius_y.track();
opacity.track();
highlight.track();
turntable.track();
layer_threshold.track();
debug_mode.track();
scene_changed.set(true);
});
on_mount(move || {
// list construction elements
const SPHERE_MAX: usize = 12;
const SPHERE_MAX: usize = 200;
let mut sphere_vec = Vec::<DVector<f64>>::new();
let color_vec = vec![
[1.00_f32, 0.25_f32, 0.00_f32],
[0.00_f32, 0.25_f32, 1.00_f32],
[0.25_f32, 0.00_f32, 1.00_f32]
[0.25_f32, 0.00_f32, 1.00_f32],
[0.25_f32, 1.00_f32, 0.00_f32],
[0.75_f32, 0.75_f32, 0.00_f32],
[0.00_f32, 0.75_f32, 0.50_f32],
];
// timing
let mut last_time = 0.0;
// scene parameters
const TURNTABLE_SPEED: f64 = 0.5;
let mut turntable_angle = 0.0;
/* INSTRUMENTS */
let performance = window().unwrap().performance().unwrap();
// get the display canvas
let canvas = display
.get::<DomNode>()
@ -207,12 +243,61 @@ fn main() {
bind_vertex_attrib(&ctx, position_index, 3, &positions);
// set up a repainting routine
let (_, start_updating_display, _) = create_raf(move || {
let (_, start_animation_loop, _) = create_raf(move || {
// get the time step
let time = performance.now();
let time_step = 0.001*(time - last_time);
last_time = time;
// move the turntable
let turntable_val = turntable.get();
if turntable_val {
turntable_angle += TURNTABLE_SPEED * time_step;
}
if scene_changed.get() {
/* INSTRUMENTS */
// measure mean frame interval
frames_since_last_sample += 1;
if frames_since_last_sample >= SAMPLE_PERIOD {
mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64));
last_sample_time = time;
frames_since_last_sample = 0;
}
// set the orientation and translation
let orientation = {
let ang_cos = turntable_angle.cos();
let ang_sin = turntable_angle.sin();
DMatrix::from_column_slice(5, 5, &[
ang_cos, 0.0, ang_sin, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0, 0.0,
-ang_sin, 0.0, ang_cos, 0.0, 0.0,
0.0, 0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 0.0, 1.0
])
};
let translation = {
const LEN: f64 = -5.0;
const LEN_SQ: f64 = LEN*LEN;
DMatrix::from_column_slice(5, 5, &[
1.0, 0.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0, LEN,
0.0, 0.0, 2.0*LEN, 1.0, LEN_SQ,
0.0, 0.0, 0.0, 0.0, 1.0
])
};
let construction_to_world = &translation * orientation;
// update the construction
sphere_vec.clear();
sphere_vec.push(engine::sphere(0.5, 0.5, -5.0 + ctrl_x.get(), radius_x.get()));
sphere_vec.push(engine::sphere(-0.5, -0.5, -5.0 + ctrl_y.get(), radius_y.get()));
sphere_vec.push(engine::sphere(-0.5, 0.5, -5.0, 0.75));
sphere_vec.push(&construction_to_world * engine::sphere(0.5, 0.5, ctrl_x.get(), radius_x.get()));
sphere_vec.push(&construction_to_world * engine::sphere(-0.5, -0.5, ctrl_y.get(), radius_y.get()));
sphere_vec.push(&construction_to_world * engine::sphere(-0.5, 0.5, 0.0, 0.75));
sphere_vec.push(&construction_to_world * engine::sphere(0.5, -0.5, 0.0, 0.5));
sphere_vec.push(&construction_to_world * engine::sphere(0.0, 0.15, 1.0, 0.25));
sphere_vec.push(&construction_to_world * engine::sphere(0.0, -0.15, -1.0, 0.25));
// set the resolution
let width = canvas.width() as f32;
@ -248,19 +333,20 @@ fn main() {
// draw the scene
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
});
/*
this wastes CPU time by running an animation loop, which updates the
display even when nothing has changed. there should be a way to make
Sycamore do single-frame updates in response to changes, but i
haven't found it yet
*/
start_updating_display();
// clear scene change flag
scene_changed.set(turntable_val);
} else {
frames_since_last_sample = 0;
mean_frame_interval.set(-1.0);
}
});
start_animation_loop();
});
view! {
div(id="app") {
div { "Mean frame interval: " (mean_frame_interval.get()) " ms" }
canvas(ref=display, width="600", height="600")
div(class="control") {
label(for="ctrl-x") { "Sphere 0 depth" }
@ -326,6 +412,14 @@ fn main() {
bind:valueAsNumber=highlight
)
}
div(class="control") {
label(for="turntable") { "Turntable" }
input(
type="checkbox",
id="turntable",
bind:checked=turntable
)
}
div(class="control") {
label(for="layer-threshold") { "Layer threshold" }
input(