5
\$\begingroup\$

Context

I am currently in the process of throwing myself into learning graphics programming, and my chosen platform is using JavaScript and WebGPU. After successfully making a basic glTF JSON renderer I found that there was much re-factoring needed of my code. In order to break down this task I have chosen to attack solely problem of coding basic UI for interacting with a (webgpu) canvas to manipulate the view of whatever geometry is being rendered (in my test case a simple triangle).

Problem

For my test case there are three types of UI interactions I wish to implement:

  1. Dragging on the canvas (with the ctrl-key up) to pan the camera in he plane orthogonal to the current way the camera is facing.
  2. Dragging on the canvas (with the ctrl-key down) to perform an arc-ball rotation.
  3. scrolling forward and backwards on the mouse wheel to zoom in and out.

My Code / Solution

My solution feels rather naïve in the extent that I have simply written down in code the solution that feels most simple for me to implement - for I don't have any good resources on how to tackle UI problems (if you know any good ones please do include in any response), so I suspect that what I have written is suboptimal in many ways!

Specifically what I have done is create three objects that track the state of the three different UI tasks (which are controlled by event listeners). Then, when the appropriate event fires under the appropriate state we update a WebGPU buffer that is passed to a rendering function that transforms the vertex data accordingly.

Code is as follows:

HTML

<!doctype html>

<html>
    <head>
        <h1>WebGPU UI Test</h1>
    </head>
    <body>
        <div>
            <!--The canvas for our UI test-->
            <canvas id="my_webgpu_canvas_id" ></canvas>
        </div>
    </body>
</html>

WebGPU Rendering Code


<!-- This scripts provides the functionality to initilise rendering a triangle on a given canvas-->
<script>
    // This function prepares the rendering of a simple triangle on the canvas context webgpu context `webgpu_context`, where we use the webgpu device `webgpu_device` to perform all gpu computation. 
    // The rendering of the triangle is subject to an array of mat4x4 transforms `global_transforms` that are left-applied (in the order they are given) to the vertex data to allow the scene to be transformed (by the user).
    // We assume that the global_transform is managed externally to the scene that this function call sets up. 
    // Ergo we do not need to perform modification to it, only wrap it up into a bind group and send it to the shader.
    // Nevertheless the elements of  global_transform MUST be a valid webgpu_device buffer that encodes the data of a mat4x4<f32> datatype at the beginning of its buffer.
    //
    //This is mostly webgpu boiler-plate
    async function render_scene(webgpu_context, webgpu_device,global_transforms){

        //Configure the webgpu-context
        let webgpu_swapchain_format = "bgra8unorm";
        let webgpu_context_config={device:webgpu_device, format:webgpu_swapchain_format, usage:GPUTextureUsage.RENDER_ATTACHMENT, alphaMode:"premultiplied"}
        webgpu_context.configure(webgpu_context_config);

    //  -------------------------------------------------------------
    //  Setting up the shader (module) for rendering

        //Defining data for the shader
        let [vertex_location] = [0];
                        let vertex_entry_point_identifier = "vertex_main";
                        let fragment_entry_point_identifier = "fragment_main";

        //This variable defines the bindings for the buffers giving the global transforms in the WGSL shader code
        let global_transform_uniform_definition_code = global_transforms.map((e,i,a) => `@group(0) @binding(${i}) var<uniform>     transform_${i}: mat4x4<f32>;`).join(``);
        //This variable defines the composit transformation of all our globals that we apply to our position data in our WGSL shader.
        let global_transform_WGSL = global_transforms.map((e,i,a) => `transform_${i}`).reduce((a,c) => a + ` * ` + c);
        let my_webgpu_shader_code = 
        
        //Create the shader code
        `
        ${global_transform_uniform_definition_code}
        
struct vertex_stage_struct {
    @location(${vertex_location}) position_data : vec4<f32>,
}

struct fragment_stage_struct {
    @builtin(position)              position_data   : vec4<f32>,
}


@fragment
fn ${fragment_entry_point_identifier}() -> @location(0) vec4<f32> {
    return vec4<f32>(1,1,1,1);
};



@vertex
fn ${vertex_entry_point_identifier}(input : vertex_stage_struct) -> fragment_stage_struct {
    var output : fragment_stage_struct;
    output.position_data    =  vec4<f32>(0,0,0.5,0) +  ${global_transform_WGSL} * input.position_data;
    return output;
}
`
        // Finally construct the shader module
        let webgpu_shader_module_config={code:my_webgpu_shader_code};
            let my_webgpu_shader_module=await webgpu_device.createShaderModule(webgpu_shader_module_config);
    //  -------------------------------------------------------------
        //Set up the vertex and fragment states
        let my_webgpu_vertex_state={module:my_webgpu_shader_module, entryPoint:`${vertex_entry_point_identifier}`, buffers:[{arrayStride: 4*4, attributes:[ {format: "float32x4", offset:0, shaderLocation:vertex_location}]}]};
        let my_webgpu_fragment_state={module:my_webgpu_shader_module, entryPoint:`${fragment_entry_point_identifier}`, targets:[{format:webgpu_swapchain_format}]};
        //Set up the depth Stencil
        let my_webgpu_depth_format="depth24plus-stencil8";
        let my_webgpu_depth_texture_config={size:{width:webgpu_context.canvas.width, height:webgpu_context.canvas.height, depth:1}, format:my_webgpu_depth_format, usage: GPUTextureUsage.RENDER_ATTACHMENT};
        let my_webgpu_depth_texture = await webgpu_device.createTexture(my_webgpu_depth_texture_config);
        //Set Up Bind-Groups for the global transform, using the externally managed buffers global_transforms:
        let my_transform_bind_group_layout_config={entries: global_transforms.map((e,i,a) => ({binding: i, visibility: GPUShaderStage.VERTEX , buffer: {type: "uniform"}}))};
        let my_transform_bind_group_layout = webgpu_device.createBindGroupLayout(my_transform_bind_group_layout_config);
        let my_transform_bind_group_config={layout:my_transform_bind_group_layout, entries: global_transforms.map((e,i,a) =>  ({binding:i, resource:{buffer:  e}}))};
        let my_transform_bindgroup = webgpu_device.createBindGroup(my_transform_bind_group_config);
        //Set up the Pipeline
        let my_webgpu_pipeline_layout_config= {bindGroupLayouts:[my_transform_bind_group_layout]};
        let my_webgpu_pipeline_layout=webgpu_device.createPipelineLayout(my_webgpu_pipeline_layout_config);
        let my_webgpu_render_pipeline_config={layout:my_webgpu_pipeline_layout, vertex: my_webgpu_vertex_state, fragment:my_webgpu_fragment_state, depthStencil:{format: my_webgpu_depth_format,depthWriteEnabled: true, depthCompare:"less"}, unclippedDepth:true};
        let my_webgpu_render_pipeline=webgpu_device.createRenderPipeline(my_webgpu_render_pipeline_config);
        //Create the View
        let my_webgpu_view=my_webgpu_depth_texture.createView();
        //Set Up the Render-Pass descriptor
        let my_webgpu_render_pass_descriptor = {colorAttachments:[{view:undefined, loadOp:"clear",clearValue:[0,0,0,1], storeOp:"store"}], depthStencilAttachment: {view: my_webgpu_view, depthLoadOp:"clear", depthClearValue:1, depthStoreOp:"store", stencilLoadOp:"clear", stencilClearValue:0, stencilStoreOp:"store"}};
        //Set up a webgpu buffer to store our vertex-data for a basic `debug` triangle
        let number_of_vertexes = 3;
        let position_buffer_config = {size: 16 * number_of_vertexes, usage:GPUBufferUsage.VERTEX, mappedAtCreation: true};
        let position_buffer=webgpu_device.createBuffer(position_buffer_config);
            new Float32Array(position_buffer.getMappedRange()).set([[1,-1,0,1],[-1,-1,0,1],[0,1,0,1]].flat());
            position_buffer.unmap();
        //Set up the actual function that performs rendering        
        let my_webgpu_frame = function(){
            my_webgpu_render_pass_descriptor.colorAttachments[0].view = webgpu_context.getCurrentTexture().createView();
            let my_webgpu_command_encoder = webgpu_device.createCommandEncoder();
                let my_webgpu_render_pass=my_webgpu_command_encoder.beginRenderPass(my_webgpu_render_pass_descriptor);
                    my_webgpu_render_pass.setBindGroup(0, my_transform_bindgroup);
                    my_webgpu_render_pass.setPipeline(my_webgpu_render_pipeline);
                    my_webgpu_render_pass.setVertexBuffer(0,position_buffer);
                    my_webgpu_render_pass.draw(number_of_vertexes,2,0,0,);
                my_webgpu_render_pass.end();
        webgpu_device.queue.submit([my_webgpu_command_encoder.finish()]);
        // We wish to contiunelly render the scene - so we use a callback to re-render when the .
        requestAnimationFrame(my_webgpu_frame);
        }
//Begin Rendering
requestAnimationFrame(my_webgpu_frame);
}

UI Handling Code

</script>
<!-- This script handles initiating the rendering and setting up the simple UI when the document loads-->
<script>
    // Aqqurie the canvas DOM element from the HTML that we wish to render to
    let my_webgpu_canvas = document.getElementById("my_webgpu_canvas_id");
    //  This DOMRect gives us information on the size of the canvas, which we need to transform mouse event data in to co-ordinates normalised to [-1,1].
    //  We use a square of unit lengh on both co-ordinate axis as that what the Webgpu spec expects.
    //  We use a function instead of storing a variable because if properties of the canvas change (notebly the height and width), we need to update this information.
    let my_canvas_DOMRect = () => my_webgpu_canvas.getBoundingClientRect();

    //  Many WebGPU Api calls are async and we must await them. As await cannot be used at the top level we use an event listener to trigger the code when the page loads
    document.addEventListener("DOMContentLoaded",async () => {      
        // Aquire a webgpu devicse so we can perform rendering and GPU compute
        let my_webgpu_gpu = await navigator.gpu;
        let my_webgpu_adaptor = await my_webgpu_gpu.requestAdapter();   
        let my_webgpu_device = await my_webgpu_adaptor.requestDevice();
        // Prepeare the canvas for rendering by setting up a webgpu context
        let my_webgpu_context = my_webgpu_canvas.getContext("webgpu");
        //We have four buffers to codify our global transform that the user can apply via UI: 
            //  one temporary rotation buffer `transiant_rotation` that is used to encode a rotation that the user is currently constructing via an arcball
            //  one temporary buffer `transiant_translation` that  that is used to encode a translation that the user is currently constructing by dragging on the canvas.
            //  one buffer `scale_transform` that encodes a zoom that the user selects using the mouse-wheel
            // finally one `committed` buffer that encodes the final result after all tranlations have been confirmed by the user (by finishing their interaction with the ui). (i.e. the totality of all transformations the user has applied all applied together)
        // When a user confirms the transform by however that is encoded in the ui, then the transient buffer is left-applied to the committed buffer before being reset to the identity. (this is explicitly codified in the WGSL code confirmation_module_code given below).
        // One deviation from this paradigm is that we store the net zoom in its own buffer (scale_transform) and never compose it into my_confirmed_transform. We do this as repeated zooming in and out may cause loss of numerical precision unnesseserraly.
        // This does not cause any issues with regards to the order of transform application (read: matrix multiplication) as a the zoom matrix commutes with all of the other matrixies in play.
        let transform_buffer_config =  {size: 16*4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,  mappedAtCreation: true};
        let Transformations =   Array.from ({length: 4},(e,i,a) => my_webgpu_device.createBuffer( transform_buffer_config));
            let   [transiant_translation,transiant_rotation,my_confirmed_transform,scale_transform]= Transformations;
            //We Initilise our transform to the identity matrix as it provides the correct default behaviour (i.e. do not affect the render in any way)
            Transformations.map(z => {new Float32Array(z.getMappedRange()).set([[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]].flat()); z.unmap()});

    //  -------------------------------------------------------------
    //  Defining the WebGPU shaders and associated JavaScript functions to implement the ability to update transformation buffers and to combine / collate transformations into the my_confirmed_transform.
        let confirmation_module_code =
`
@group(0) @binding(0) var<storage, read_write> mat_0    :  mat4x4<f32>;
@group(0) @binding(1) var<storage, read_write> mat_1    :  mat4x4<f32>;
    @compute @workgroup_size(1)
fn my_fn(){

    mat_0 = mat_1 * mat_0;
    mat_1 = mat4x4<f32>(1,0,0,0,    0,1,0,0,    0,0,1,0,    0,0,0,1);
}
`

        let confirmation_module_config = {code: confirmation_module_code};
        let update_module = my_webgpu_device.createShaderModule(confirmation_module_config);


        let confirmation_bind_group_layout_config={entries:[[0,"storage"],[1,"storage"]].map(([i,j]) => ({binding:i, visibility: GPUShaderStage.COMPUTE,  buffer: { type: j}}))};
        let confirmation_bind_group_layout = my_webgpu_device.createBindGroupLayout(confirmation_bind_group_layout_config);
    


        let my_pipeline_layout_config = {bindGroupLayouts:[confirmation_bind_group_layout]};
            let my_pipeline_layout = my_webgpu_device.createPipelineLayout(my_pipeline_layout_config);
            let my_pipeline_config = {layout: my_pipeline_layout, compute: {module: update_module, entryPoint: `my_fn`}};
            let my_pipeline = my_webgpu_device.createComputePipeline(my_pipeline_config);

        //Takes a 16-element array `x` in column-normal form and updates the buffer `uniform`
        let  set_uniform =  async (x,uniform) => {
                
                //create a tempory buffer that can update our uniforms' data
                let transform_update_buffer_config =  {size: 16* 4, usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE , mappedAtCreation: true};
                    let my_uniform_update_buffer = my_webgpu_device.createBuffer(transform_update_buffer_config);
                        new Float32Array(my_uniform_update_buffer.getMappedRange()).set(x);
                    my_uniform_update_buffer.unmap();
                let my_command_encoder = my_webgpu_device.createCommandEncoder();
                        my_command_encoder.copyBufferToBuffer(my_uniform_update_buffer,0,uniform,0,16 * 4);
                let my_command_buffer = my_command_encoder.finish()
                 my_webgpu_device.queue.submit([my_command_buffer]);
        };

        
        // Runs the webgpu code `confirmation_module_code` with `transformation` in binding 0 to `confirm` the transformation as described prior
        let confirm_transformation = async (transformation) => {
            let confirmation_bind_group_bindings=[{binding:0, resource: {buffer: my_confirmed_transform}},{binding:1, resource: {buffer: transformation}}];
            let confirmation_bind_group = my_webgpu_device.createBindGroup({layout: confirmation_bind_group_layout, entries: confirmation_bind_group_bindings});
            await  my_webgpu_device.queue.onSubmittedWorkDone().then(  () => {
            let my_command_encoder = my_webgpu_device.createCommandEncoder();
            let my_compute_pass_encoder =  my_command_encoder.beginComputePass();
            my_compute_pass_encoder.setPipeline(my_pipeline);
                my_compute_pass_encoder.setBindGroup(0,confirmation_bind_group);
                my_compute_pass_encoder.dispatchWorkgroups(1);
                my_compute_pass_encoder.end();
            let my_command_buffer = my_command_encoder.finish()
            my_webgpu_device.queue.submit([my_command_buffer]);})
        }

    //  -------------------------------------------------------------
    //  Defining Assorted Helper functions for working with the co-ordiate systems relative to the mouse-events and to the WebGPU normalised device co-ordinates.
        // Two Helper Functions for translating the normalised mouse-co-ordinates into a mat4x4 transformation representing a translation operation.
            // A Helper function that computes the translation in webgpu normalised device coordinates between two points in our normalised rectangle.
            // Note that the  y-cordinate is reversed (multiplied by -1) due to the skew-symmetry between how javascript stores y co-ordintate with increasing y moving downwards whereas how webgpu has normilised-device-coordinates where increasing y moves upwards. (both agree on the x co-ordinate)
            let compute_NDC_translation = ([a,b],[c,d]) => [c - a, b - d];
            // Our webgpu shaders all expect all transformations to of type be mat4x4, this helper function provides the explicit transformation from a vec2 translation.
            let convert_translation_to_transformation = ([a,b]) => [[1,0,0,0],[0,1,0,0],[0,0,1,0],[a,b,0,1]].flat();
            // A simple helper function that translates the XY co-ordinates from a mouse-event to normalied co-ordinates in [-1,1] x [-1,1]. 
            // In the edge case of 0 height width (which should never happen because it means there is no canvas to display) we simply default to the broken co-ordinate to the origin. 
            let normalise_to_rect = ([x,y],rect) => [ 2 * x / (rect.width) - 1 || 0, 2 * y / (rect.height) - 1 || 0];
            // This Helper function skimms the (offset) mouse-coordinates from an event
            let get_event_coords = (event) => [event.offsetX,event.offsetY]

    //  -------------------------------------------------------------
    //  Defining Objects to keep track of the state of the users scrolls (for zoom), and drags for (pans and rotations)

        //  A small object to manage the state associated to the user performing a drag operation inside of the canvas. This is a simple way of monitering a drag: we don't use debouncing here because there is no action indended for a slimple click event,
        //  and a small accidental drag only slightly moves the image and can be easily reversed.
        let drag_state = {
            //Used to signify if the user is currently in the middle of a drag-operation
            dragging: false,
            //Used to store the normalised (x,y) mouse position at the start of a drag
            drag_start:  null,
            //Used to store the most recent position in the drag
            drag_current: null,
        }
        // The rotate_state object manages the state associate associated to the drag operation for the arcball rotation. It works in the same way.
        let rotate_state = {
        rotating: false,
        rotate_start: null,
        rotate_current: null,
        }

        // The scroll_state object manages the state associated to the scrolling for zoom. Unlike a drag event we don't need to store temporal data  through an ongoing user input: just the cumulative amount they have scrolled
        let scroll_state = {
            amount: 0n,
        }

        //  scroll_base is the base of the exponent used for calculating the level of zoom
        let scroll_base = 1.1;
    //  -------------------------------------------------------------
        //Add Listeners to facilitate the drag-scroll_based behaviour
        my_webgpu_canvas.addEventListener(`mousedown`, (e) => {
            if(!e.ctrlKey && e.button == 0){
                                                                let normalised_x_y = normalise_to_rect(get_event_coords(e),my_canvas_DOMRect());
                                                                drag_state.drag_start= normalised_x_y;
                                                                drag_state.drag_current  = normalised_x_y;
                                                                drag_state.dragging = true;
                                                            }
                                                                });
        my_webgpu_canvas.addEventListener(`mousemove`,  (e) => {
                                                                
            drag_state.drag_current = normalise_to_rect(get_event_coords(e),my_canvas_DOMRect());
                                                                console.log()
                                                                if (drag_state.dragging)
                                                                set_uniform(convert_translation_to_transformation(compute_NDC_translation(drag_state.drag_start,drag_state.drag_current)),transiant_translation)

                                                                
                                                                ;
        });
        //We listen for mouse-up on the entire window and not just the canvas so that if the user mouse-ups with the curse outside the canvas the drag still concludes.
        self.addEventListener(`mouseup`, async  (e) => {
            if(drag_state.dragging && e.button == 0){
            drag_state.dragging = false;
            confirm_transformation(transiant_translation);
            }
        })

    //  -------------------------------------------------------------
    // Code that impliments the scroll-zoom control for the canvas.
        // Calculates the scaling (zoom) amount from the net scroll
        let get_scale = () =>  scroll_base  ** x;
    let convert_scroll_to_transformation = (x) => [[x,0,0,0],[0,x,0,0],[0,0,x,0],[0,0,0,1]].flat()
    my_webgpu_canvas.addEventListener(`wheel`, (e) => {
        //Update the cumulative scroll amoount
        scroll_state.amount -= BigInt( Math.sign(e.deltaY)); 
        // find the new required level of zoom
        scale = scroll_base** Number(scroll_state.amount); 
        // Updates the WebGPU uniform that manages the zoom transformation
        set_uniform(convert_scroll_to_transformation(scale),scale_transform)
        // Prevent the browser from doing its own ui adjustments: this is mainly to avoid zooming the page out if the ctrl key is being held in most browsers.
        e.preventDefault()
    })
    //  -------------------------------------------------------------
    // Code that impliments the arcball camarea for the canvas.

        // This WGSL shader code takes two vec3s and computes the transformation (as a vec4x4) that would take the normalised first to the the normalised second (which is the main computation in an arcball camera).
        //  It is based off of Rodrigues' formula.
        let arc_update_code = 
`
@group(0) @binding(0) var<storage, read_write> from_vect    :  vec3<f32>;
@group(0) @binding(1) var<storage, read_write> to_vect      :  vec3<f32>;
@group(0) @binding(2) var<storage, read_write> tr           :  mat4x4<f32>;
    @compute @workgroup_size(1)
fn my_fn(){
    var normal_vect = cross(to_vect,from_vect);
    var c = dot(from_vect,to_vect);
    var s = length(normal_vect);
    var skewm = mat4x4<f32>(0f, normal_vect[2], -normal_vect[1], 0f,        -normal_vect[2], 0f, normal_vect[0], 0f,        normal_vect[1], -normal_vect[0],0f,0f, 0,0,0,0);
    tr = mat4x4<f32>(1,0,0,0,       0,1,0,0,    0,0,1,0,    0,0,0,1) + skewm  + (1 - c) * (skewm * skewm);
}

`
    //The WebGPU config for the vec3 buffers that will represent co-ordinates on the arcball
    let let_arc_vector_buffer_config =  {size: 3*4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,  mappedAtCreation: false};
    //Said buffers
    let arc_from_vector =  my_webgpu_device.createBuffer(let_arc_vector_buffer_config);
    let arc_to_vector =  my_webgpu_device.createBuffer(let_arc_vector_buffer_config);

    // Compile the WGSL code into a shader module
    let arc_confirmation_module_config = {code: arc_update_code};
    let  arc_update_module = my_webgpu_device.createShaderModule(arc_confirmation_module_config);

    //Sort out the bindings and set up a pipeline
    let my_arc_bind_group_layout_config={entries:[[0,"storage"],[1,"storage"],[2,"storage"]].map(([i,j]) => ({binding:i, visibility: GPUShaderStage.COMPUTE,  buffer: { type: j}}))};
        let my_arc_bind_group_layout = my_webgpu_device.createBindGroupLayout(my_arc_bind_group_layout_config);
    let my_arc_bind_group_bindings=[{binding:0, resource: {buffer: arc_from_vector}},{binding:1, resource: {buffer: arc_to_vector}},{binding:2, resource: {buffer: transiant_rotation}}];
        let my_arc_bind_group = my_webgpu_device.createBindGroup({layout: my_arc_bind_group_layout , entries: my_arc_bind_group_bindings});
    let my_arc_pipeline_layout_config = {bindGroupLayouts:[my_arc_bind_group_layout]};
        let my_arc_pipeline_layout = my_webgpu_device.createPipelineLayout(my_arc_pipeline_layout_config);
        let my_arc_pipeline_config = {layout: my_arc_pipeline_layout, compute: {module: arc_update_module, entryPoint: `my_fn`}};
        let my_arc_pipeline = my_webgpu_device.createComputePipeline(my_arc_pipeline_config);



    let  set_arc_uniform =  async (to_vector,from_vector) => {
        let arc_update_buffer_config =  {size: 3 * 4, usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE , mappedAtCreation: true};
            let my_arc_update_buffer_one = my_webgpu_device.createBuffer(arc_update_buffer_config);
            let my_arc_update_buffer_two = my_webgpu_device.createBuffer(arc_update_buffer_config);
                new Float32Array(my_arc_update_buffer_one.getMappedRange()).set(to_vector);
                new Float32Array(my_arc_update_buffer_two.getMappedRange()).set(from_vector);
                await my_arc_update_buffer_one.unmap();
                await my_arc_update_buffer_two.unmap();

        // Setup the command encoder to run the arc-ball computation and then update the rotation-buffer used for rendering
        let my_command_encoder = my_webgpu_device.createCommandEncoder();
            my_command_encoder.copyBufferToBuffer(my_arc_update_buffer_one,0,arc_from_vector,0,3 * 4);
            my_command_encoder.copyBufferToBuffer(my_arc_update_buffer_two,0,arc_to_vector,0,3 * 4);
            let my_compute_pass_encoder =  my_command_encoder.beginComputePass();
                my_compute_pass_encoder.setPipeline(my_arc_pipeline);
                my_compute_pass_encoder.setBindGroup(0,my_arc_bind_group);
                my_compute_pass_encoder.dispatchWorkgroups(1);
            my_compute_pass_encoder.end();
        let my_command_buffer = my_command_encoder.finish()
        my_webgpu_device.queue.submit([my_command_buffer]);
    }

    // A helper function that takes a normalised co-ordinate in [-1,1] x [-1,1] onto its associated point on the sphere.
    let xy_to_sphere = ([x,y]) => { let r = x ** 2 + y** 2 ;return (r <= 1) ? [x,y,Math.sqrt(1 - r)] : [x/Math.sqrt(r),y/Math.sqrt(r),0]};

    //  Listener to process the start of a drag
    my_webgpu_canvas.addEventListener('mousedown', (e) => {
        if(e.ctrlKey && e.button == 0){
            console.log(e.ctrlKey)
            let normalised_x_y = normalise_to_rect(get_event_coords(e),my_canvas_DOMRect());
            rotate_state.rotating = true;
            rotate_state.rotate_start = normalised_x_y;
            rotate_state.rotate_current = normalised_x_y;
    }})

    // Listener to process when the mouse moves whilst a drag is occuring
    my_webgpu_canvas.addEventListener('mousemove', (e) => {
        if(e.ctrlKey && rotate_state.rotating){
            rotate_state.rotate_current = normalise_to_rect(get_event_coords(e),my_canvas_DOMRect());
            //console.log(xy_to_sphere(rotate_state.rotate_start),xy_to_sphere(rotate_state.rotate_current))
            set_arc_uniform(xy_to_sphere(rotate_state.rotate_start),xy_to_sphere(rotate_state.rotate_current))
            
    }})
    
    //These two event listeners process when to stop proccesing a drag (wither when the ctrl key is raised or when the mouse )
    self.addEventListener("keyup", (e) => {
        if(e.key == "Control")
        rotate_state.rotating = false;
        confirm_transformation(transiant_rotation);
    });

    self.addEventListener(`mouseup`,  (e) => {
        rotate_state.rotating = false;
        confirm_transformation(transiant_rotation);
    })
//  Now that all the required objects have been constructed we may get to rendering our scene.
render_scene(my_webgpu_context,my_webgpu_device,Transformations)
});
</script>

concatenating the above strings together in the order they are given produces an HTML file that is my solution, which can be run on any browser so long as it has been set-up to allow WebGPU content to run (at time of writing one must set chrome flags to allow it).

What I would mostly like Comments on

  1. approach:

    Is my approach to a solution the UI problem, in general, the best for the platform I am using - if not what other pattern should I consider adopting - if so are there any areas in which my implementation is coded poorly and could be improved?

  2. Coupling:

    This note in particular I would love detailed comment on. In writing and thinking about my code I note two areas where different objects / areas of concern are connected to each other. Firstly, the three different objects that I have used to record the ongoing state of the three ui interactions (rotate_state, scroll_state & drag_state), and secondly I have some WebGPU buffers defined in the section / function that handles UI that gets sent to the section that handles rendering:

    Firstly Is how the UI state is handled scalable, I can Imagine if I added more and more elements of UI in the way that I currently am that the solution would quickly become a convoluted mess - especially if there need be any communications between ui operations that are undertook. I had thought to wrap each of the three objects up into JavaScript Classes but decided against it because it doesn't solve the problem just adds boiler-plate to my eyes at least.

    I don't have a specific question in mind for the way that information is sent from the UI to the rendering function, but I am asking for optional comment on my graphics code later on and if anyone wishes to answer that with some comment on this area would be appreciated.

    1. General quality / writing of the code.

    Programming mostly for a hobby means I get no feedback on my style or if I do any bad practices, so would appreciate comment to that affect if there is anything of note.

Extra Stuff I would like comment on but is Secondary

  • use of WebGPU API

    As I am just getting into graphics programming, I don't really have much of an idea if the way that I write code is idiomatic or if it flawed in some way. Additionally, being as the api I am using is still in active development there is less in the way of targeted literature and tutorials for it. This is an open ended question - but If anyone has comment on how I am setting up my graphics pipelines I would love to hear it.

  • Are my comments useful?

    I generally aim to write comments with the aim of explaining the program semantics and design choices to someone unfamiliar with the code (i.e. future me). Are my comments useful in this regard? If you have any feedback on how they are written please add!

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.