// based on the WebGL example in the `wasm-bindgen` guide // // https://rustwasm.github.io/wasm-bindgen/examples/webgl.html // // and this StackOverflow answer by wangdq // // https://stackoverflow.com/a/39684775 // extern crate js_sys; use sycamore::{prelude::*, rt::{JsCast, JsValue}}; use web_sys::{console, WebGl2RenderingContext, WebGlShader}; fn compile_shader( context: &WebGl2RenderingContext, shader_type: u32, source: &str, ) -> WebGlShader { let shader = context.create_shader(shader_type).unwrap(); context.shader_source(&shader, source); context.compile_shader(&shader); shader } // load the given data into the vertex input of the given name fn bind_vertex_attrib( context: &WebGl2RenderingContext, index: u32, size: i32, data: &[f32] ) { // create a data buffer and bind it to ARRAY_BUFFER let buffer = context.create_buffer().unwrap(); context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer)); // load the given data into the buffer. the function `Float32Array::view` // creates a raw view into our module's `WebAssembly.Memory` buffer. // allocating more memory will change the buffer, invalidating the view. // that means we have to make sure we don't allocate any memory until the // view is dropped unsafe { context.buffer_data_with_array_buffer_view( WebGl2RenderingContext::ARRAY_BUFFER, &js_sys::Float32Array::view(&data), WebGl2RenderingContext::STATIC_DRAW, ); } // allow the target attribute to be used context.enable_vertex_attrib_array(index); // take whatever's bound to ARRAY_BUFFER---here, the data buffer created // above---and bind it to the target attribute // // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer // context.vertex_attrib_pointer_with_i32( index, size, WebGl2RenderingContext::FLOAT, false, // don't normalize 0, // zero stride 0, // zero offset ); } fn main() { // set up a config option that forwards panic messages to `console.error` #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); sycamore::render(|| { let ctrl_x = create_signal(0.0); let ctrl_y = create_signal(0.0); let display = create_node_ref(); on_mount(move || { // get the display canvas let canvas = display .get::() .unchecked_into::(); let ctx = canvas .get_context("webgl2") .unwrap() .unwrap() .dyn_into::() .unwrap(); // compile and attach the vertex and fragment shaders let vertex_shader = compile_shader( &ctx, WebGl2RenderingContext::VERTEX_SHADER, r##"#version 300 es in vec4 position; void main() { gl_Position = position; } "##, ); let fragment_shader = compile_shader( &ctx, WebGl2RenderingContext::FRAGMENT_SHADER, r##"#version 300 es precision highp float; out vec4 outColor; uniform vec2 resolution; uniform float shortdim; uniform vec2 ctrl; struct vecInv { vec3 sp; vec2 lt; }; vecInv sphere(vec3 center, float radius) { return vecInv( center / radius, vec2( 0.5 / radius, 0.5 * (dot(center, center) / radius - radius) ) ); } const float focal_slope = 0.3; const vec3 light_dir = normalize(vec3(2., 2., 1.)); void main() { vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; vec3 dir = vec3(focal_slope * scr, -1.); vecInv v = sphere(vec3(ctrl, -5.), 1.); float a = -v.lt.s * dot(dir, dir); float b = dot(v.sp, dir); float c = -v.lt.t; float scale = -b/(2.*a); float adjust = 4.*a*c/(b*b); float offset = sqrt(1. - adjust); float u_front = scale * (1. - offset); float u_back = scale * (1. + offset); vec3 color; if (adjust < 1. && u_front > 0.) { // the expression for normal needs to be checked. it's // supposed to give the negative gradient of the lorentz // product between the impact point vector and the sphere // vector with respect to the coordinates of the impact // point. i calculated it in my head and decided that // the result looked good enough for now vec3 pt_front = u_front * dir; vec3 normal_front = normalize(-v.sp + 2.*v.lt.s*pt_front); float incidence = dot(normal_front, light_dir); if (incidence < 0.) { color = mix(vec3(0.2, 0.0, 0.4), vec3(0.1, 0.0, 0.2), -incidence); } else { color = mix(vec3(0.4, 0.0, 0.2), vec3(1., 0.8, 1.), incidence); } } else { color = vec3(0.); } outColor = vec4(color, 1.); } "##, ); let program = ctx.create_program().unwrap(); ctx.attach_shader(&program, &vertex_shader); ctx.attach_shader(&program, &fragment_shader); ctx.link_program(&program); let link_status = ctx .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS) .as_bool() .unwrap(); let link_msg = if link_status { "Linked successfully" } else { "Linking failed" }; console::log_1(&JsValue::from(link_msg)); ctx.use_program(Some(&program)); // find indices of vertex attributes and uniforms let position_index = ctx.get_attrib_location(&program, "position") as u32; let resolution_loc = ctx.get_uniform_location(&program, "resolution"); let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); let ctrl_loc = ctx.get_uniform_location(&program, "ctrl"); // create a vertex array and bind it to the graphics context let vertex_array = ctx.create_vertex_array().unwrap(); ctx.bind_vertex_array(Some(&vertex_array)); // set the vertex positions const VERTEX_CNT: usize = 6; let positions: [f32; 3*VERTEX_CNT] = [ // northwest triangle -1.0, -1.0, 0.0, -1.0, 1.0, 0.0, 1.0, 1.0, 0.0, // southeast triangle -1.0, -1.0, 0.0, 1.0, 1.0, 0.0, 1.0, -1.0, 0.0 ]; bind_vertex_attrib(&ctx, position_index, 3, &positions); // set the resolution let width = canvas.width() as f32; let height = canvas.height() as f32; ctx.uniform2f(resolution_loc.as_ref(), width, height); ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); // set up a repainting routine create_effect(move || { // pass the control parameters ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); // clear the screen and draw the scene ctx.clear_color(0.0, 0.0, 0.0, 1.0); ctx.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT); ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); }); }); view! { div(id="app") { canvas(ref=display, width="600", height="600") input( type="range", min=-1.0, max=1.0, step=0.01, bind:valueAsNumber=ctrl_x ) input( type="range", min=-1.0, max=1.0, step=0.01, bind:valueAsNumber=ctrl_y ) } } }); }