Slider Changes Re-Creating WebGL Context?

Hello again!

I have the following article, and I’ve been trying to change the thickness function to use viewof and <input type='range'>. However, after doing that (and updating the code in the render loop), every time the slider changes it seems to re-create the WebGL context (visible in the console with ThreeJS logs). Is there a way to achieve this in a more performant way, i.e. just treating it as a variable being changed?

https://beta.observablehq.com/@mattdesl/2d-quadratic-curves-on-the-gpu

Thanks!

1 Like

I made a copy and got it to work. I haven’t published it, so not sure if you can see it (probably not?) but the link is here (let me know if you need me to publish it).

https://beta.observablehq.com/d/ddc5ea52009214da

Here are the changes:

In the main ‘create’ cell I created a function which receives the thickness change as a parameter. You will call this function when the thickness changes, and this avoids reevaluating the whole cell - in general you want to avoid relying on dynamic values that are going to change in the main body of code in that cell, as it will cause the cell to reexecute - so instead we use a callback function to handle it.

    function changeThickness(t) {
       mesh.material.uniforms.thickness.value = t;
     }

I also modified the return from this cell to return a dictionary instead of just the dom element (I borrowed this trick from John Bannister’s “planets” demo, so you can get a reference to the callback function.

return {'element':renderer.domElement, 'changeThickness':changeThickness};

This means you have to add a separate block to display the actual element, since you’re returning a dictionary instead of a dom element.

create.element

Then I added a slider (using the slider import at the bottom)

    // import
    import {slider} from "@jashkenas/inputs"

    // slider
    viewof thickness = slider({min: 0.01, max: .3, value: .1, description: "Thickness"})

And I added a cell to call the callback function when the slider is moved (would be cool if this were a parameter to the slider, hmmm…)

    create.changeThickness(thickness)

Note that this shows up as ‘undefined’. To make it look a little nicer, you can have it redisplay the thickness, like so:

    { create.changeThickenss(thickness); return thickness; }

Also note that I’m using identical shenanigans to get the slider to work on my sphere demo, here:

https://beta.observablehq.com/@jbum/covering-a-sphere-with-near-equidistant-dots\

I suspect there is a more elegant way to do this, perhaps by breaking up the cell with most of the code into separate cells. I’ll play around with it…

I found an alternate way to do it that doesn’t require a callback function or a dictionary. Basically you break out the scene and the mesh as separate cells, so they aren’t internal to your ‘create’ cell. Here’s the example:

https://beta.observablehq.com/@jbum/mesh-modification-alt-method

EDIT: I deleted this since Mike’s example is much cleaner. Also updated my sphere demo to use this style.

Here’s an adaptation of your notebook that demonstrates how to mutate WebGL state:

In a sense, WebGL is a “worst-case scenario” for Observable because the API is inherently mutable: there’s a global WebGL context, and you need to mutate that context to render efficiently. Observable’s preference is for immutable state and pure functions, where you simply throw away the old state when referenced values change, and then create new start from the new referenced values.

But, since each cell in Observable is “just JavaScript”, you can totally use side-effects and mutation, as long as you understand the data flow graph of your notebook: how the cells depend on each other, and thus how changes in one cell will ripple through the other cells in your notebook.

Your notebook’s data flow graph looks like this:

Because the create cell depends on thickness, any change to thickness (such as making it a slider and interacting with it) causes the create cell to be re-run, throwing away your THREE.WebGLRenderer and other objects, and creating new ones from scratch.

In contrast, here’s the data flow graph in my notebook:

So here, thickness only feeds into render. The render cell then mutates the mesh and re-renders the scene, allowing it to render efficiently because it’s mutating the existing state rather than re-creating it from scratch.

The other way you preserve existing state rather than throwing it away is to use this, which stores the previous value of the current cell. Tom’s written a notebook on this values for more on that subject, and I’ll probably write more notebooks on the subject in the future, too.

(If you saw my earlier dependency graphs, note that I’ve inverted the orientation of edges in the graphs above in a way I hope is clearer: the arrows above represent the flow of values through the notebook, rather than the backwards references.)

5 Likes

Thanks for this Mike. I modified my sphere demo to more closely resemble your example

https://beta.observablehq.com/@jbum/covering-a-sphere-with-near-equidistant-dots

In this particular case, I need to regenerate part of the scene, so I made an extra container “particleGroup” which is intended not to mutate, so that the scene itself won’t be regenerated when the contents of particleGroup are replaced. I’m not sure if it’s having the desired effect (edit: it’s fine - I can see the UUIDs aren’t changing on the scene when I move the slider, that’s cool), although things look right.

However, I noticed that the modified sketch isn’t generating a proper thumbnail anymore (and it takes a second longer for the render to appear when launched, which is perhaps related). Any tweaks you can suggest?

The thumbnail appears to be blank because our thumbnail daemon didn’t appear to wait long enough for the sprite (disc.png) to load. Or it’s possible that our thumbnail daemon just doesn’t handle WebGL very well—we’re working on ways to make our thumbnails better.

One thing I would recommend to make the notebook load a little faster would be to use the minified bundle of Three.js:

THREE = require('three@0.89.0/build/three.min.js')

@mbostock I found your notebook very helpful, thank you. It resolved a similar problem I was having in notebooks where I have several WebGL canvases at once.

I’ve gone one step further, and made a version of your notebook using “this” to achieve a more self-contained solution:

2 Likes