Shader Reaction Diffusion confined to a specific shape

Hi,

I’m currently working on a GLSL Reaction Diffusion project using Three.js, and I’ve been using this code as a reference. I’ve successfully updated the code to run on the latest Three.js version and made several other modifications.


My main challenges are:

  1. How can I ensure that the diffusion only occurs within the confines of a specific shape or texture when the “Only inside” checkbox is checked? I want to prevent it from spreading outside. Also to be able to create many different patterns.

  2. Is it possible to make the resulting effect transparent so that I can save a PNG image of it with a transparent background?

  3. I’ve implemented scaling for the diffusion inside the fragment.glsl file, but I’m wondering if there’s a better or more efficient way to achieve this effect?

Ultimately, my goal is to have control over whether the diffusion occurs strictly within a defined shape (texture) or if it can extend beyond when the “Only inside” checkbox is unchecked.

You can access the project on CodeSandbox through this link: CodeSandbox Link.


All improvements are welcome!


fragment.glsl
My main fragment shader looks like this:

uniform int time;
uniform vec2 resolution;
uniform sampler2D start;
uniform sampler2D uTexture;
uniform float uScale;
uniform vec2 mouse;
uniform bool allowMouse;

vec2 pos;
vec2 texColor;
vec2 offset;

uniform float dA;
uniform float dB;
uniform float kill;
uniform float feed;
uniform float dT;

vec2 getLaplace() {
    // My scaling method
    vec2 offset = vec2( 1.0 ) / resolution * (uScale - 0.1);

    vec2 up = pos + vec2( 0.0, -offset.y );
    vec2 down = pos + vec2( 0.0, offset.y );
    vec2 left = pos + vec2( -offset.x, 0.0 );
    vec2 right = pos + vec2( offset.x, 0.0 );

    vec2 center = texture2D( uTexture, pos ).rg;
    vec2 colUp = texture2D( uTexture, up ).rg;
    vec2 colDown = texture2D( uTexture, down ).rg;
    vec2 colLeft = texture2D( uTexture, left ).rg;
    vec2 colRight = texture2D( uTexture, right ).rg;

    return colUp + colDown + colLeft + colRight - 4.0 * center;
}


void main( void ) {    
    vec2 uv = gl_FragCoord.xy / resolution;
    pos = gl_FragCoord.xy / resolution;
    vec2 color;

    if( time == 1 ) {
        color = vec2( 1.0, step( 0.1, texture2D( start, uv ).g ) * 0.3 );
    } else {
        vec2 texColor = texture2D( uTexture, uv ).rg;
        offset = vec2( 1.0 ) / resolution;

        float a = texColor.r;
        float b = texColor.g;
    
        // Mouse interaction
        if(allowMouse && mouse.x > 5.0 && mouse.x < resolution.x - 5.0 && mouse.y > 5.0 && mouse.y < resolution.y - 5.0 ) {
            float diff = length( gl_FragCoord.xy - mouse );
            if( diff < 8.0 ) b = ( 1.0 - smoothstep( 1.0, 8.0, diff ) ) * 0.3;
        }

        vec2 laplace = getLaplace();

        // Gray-Scott Model Equations
        float red = (dA * laplace.r) - (a * b * b) + feed * (1.0 - a);
        float green = (dB * laplace.g) + (a * b * b) - (kill + feed) * b;

        color = texColor + vec2(red, green) * dT;
    }
    
    gl_FragColor = vec4(color, 0.0, 1.0);
}


color_fragment.glsl

uniform vec2 resolution;
uniform sampler2D uTexture;
uniform vec3 color1;
uniform vec3 color2;
uniform float uScale;

void main( void ){
    vec2 uv = gl_FragCoord.xy / resolution * vec2( 0.5 );

    vec3 color = mix(color1, color2, min(texture2D(uTexture, uv).g * 2.0, 1.0));
    color = step( 0.5, color );

    gl_FragColor = vec4(color, 1.0);
}


vertex.glsl

varying vec2 vUv;
uniform vec2 resolution;
            
void main(){
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

Any guidance or suggestions would be greatly appreciated!

I haven’t looked at your code, but… r.e. your challenges:

  1. You can maybe add on a line to the simulation phase where it writes zeroes when you read 0 from from your mask texture… gl_FragColor *= texture2D(maskTexture,uv);

  2. Yeah you can render to a rendertarget, and write your mask texture to the alphachannel, then read the rendertarget on the javascript side via renderer.readRenderTargetPixels()
    write those pixels to a canvas via putImageData… and then convert to Png image…

3.I don’t know what context you meant scaling in… but if you mean scaling textures up and down, there is a 2 pass blur technique that is fast… where you gaussian blur in the horizontal axis, then blur that result again, vertically.

@manthrax Thanks for your suggestion i have now added a new uniform called uBleed this controls when the simulation should stop.

I updated the vec2 getLaplace() function by adding:

    float threshold = 0.00001;
    float edgeSoftness = 0.00001;
    float edgeFactor = smoothstep(threshold, threshold + edgeSoftness, centerColor.g);

And in the void main( void )

if (time < int(round(bleed))) {
    color = mix(color, vec2(1.0), 0.00004);
}

Is this the right approach to stop the diffusion? Now I don’t really know how to keep the movement after the diffusion has stopped. I have tried of adding some noise but it just slowly turns the screen black or fully white.

I have updated the original CodeSandbox code

I am also not sure if the getLaplace() is correctly applied. I am quite new to the shader language.