sveltejs / gl

A (very experimental) project to bring WebGL to Svelte
https://svelte.dev/repl/8d6d139a3d634c2fb1e1ff107c123dd5?version=3.16.4
MIT License
603 stars 26 forks source link

Real/currents #28

Open dancingfrog opened 4 years ago

dancingfrog commented 4 years ago

Hi @Rich-Harris,

This is a collection of commits I've made to the WebGL extension over the past couple of weeks. I'm not expecting you to accept this pull request whole-sale, but hoping you have some time to look through the different updates and see if any of them are worth incorporating into future versions of @sveltejs/gl, particularly https://github.com/Real-Currents/SvelteGL/commit/f3b0a1c9459af2266630542948c47e53e40da876#diff-32fcd5a8151257542efe4e1860086ad5

This inserts a hook (as a callback that is passed to <GL.Scene>) that allows custom routines to run during each draw cycle. This is a very powerful capability in combination with dropping a NAME definition into the vertex and fragment shaders which becomes a property on the material object (available in the callback as material.vertName and material.fragName respectively). The shaders were updated to version 3.00 while I was in there.

I also played around with a adding some new mesh types, including a planar <GL.terrain> that is basically the plane type, with a bunch more vertices in the mesh. Using the technique mentioned above I can now implement a displacement map in a custom vertex shader, like so...

EDIT: slight improvement to terrain-vert.glsl:

in vec3 position;
in vec3 normal;

out vec3 v_normal;

#if defined(has_colormap) || defined(has_specularitymap) || defined(has_normalmap) || defined(has_bumpmap)
#define has_textures true
#endif

#ifdef has_textures
in vec2 uv;
out vec2 v_uv;
#endif

#if defined(has_normalmap) || defined(has_bumpmap)
out vec3 v_view_position;
#endif

out vec3 v_surface_to_light[NUM_LIGHTS];

#ifdef has_specularity
out vec3 v_surface_to_view[NUM_LIGHTS];
#endif

#ifdef USE_FOG
out float v_fog_depth;
#endif

// So, standard @sveltejs/gl default shader so far, and then ...

#define NAME terrain-vert

// texture containing elevation data
uniform sampler2D heightMap;

void main() {
    float displacement = texture(heightMap, uv).r;

    vec3 displace_along_normal = vec3(normal * displacement);

    vec3 displaced_position = position + (0.99 * displace_along_normal);

//  vec4 pos = vec4(position, 1.0);
    vec4 pos = vec4(displaced_position, 1.0);
    vec4 model_view_pos = VIEW * MODEL * pos;

    v_normal = (MODEL_INVERSE_TRANSPOSE * vec4(normal, 0.0)).xyz;

    #ifdef has_textures
    v_uv = uv;
    #endif

    #if defined(has_normalmap) || defined(has_bumpmap)
    v_view_position = model_view_pos.xyz;
    #endif

    #ifdef USE_FOG
    v_fog_depth = -model_view_pos.z;
    #endif

    for (int i = 0; i < NUM_LIGHTS; i += 1) {
        PointLight light = POINT_LIGHTS[i];

        vec3 surface_world_position = (MODEL * pos).xyz;
        v_surface_to_light[i] = light.location - surface_world_position;

        #ifdef has_specularity
        v_surface_to_view[i] = CAMERA_WORLD_POSITION - surface_world_position;
        #endif
    }

    gl_Position = PROJECTION * model_view_pos;
}

app.svelte:

<script>
    import { onMount } from 'svelte';
    import * as GL from '@sveltejs/gl';
    import terrainVert from './shaders/custom/terrain-vert.glsl';

    export let title;

    export let color = '#F7C77B';

    let w = 1;
    let h = 1;
    let d = 1;

    const light = {};

    function adjustColor (clr, height = 1) {
        const r = parseInt('0x' + clr.substr(1, 2), 16),
          g = parseInt('0x' + clr.substr(3, 2), 16),
          b = parseInt('0x' + clr.substr(5, 2), 16);

        const hr = Math.floor(r * (height / 0.25)),
          hb = Math.floor(b * (height / 0.25));
        return Math.abs((((hr < 255) ? hr : r) << 16) + (g << 8) + ((hb < 255) ? hb : b));
    }

    let terrain;
    const terrainMap = new Image();
    const heightMap = new Image();
    terrainMap.alt = 'Terrain Texture';
    heightMap.crossOrigin = terrainMap.crossOrigin = '';

    let webgl;
    let displacementTexture = null;
    let process_extra_shader_components = (gl, material, model) => {
        // console.log("Process Extra Shader Components");
        const program = material.program;

        if (material.vertName === "terrain-vert") {
            // console.log(material.vertName);

            if (!!displacementTexture) {
                const displacementTextureLocation = gl.getUniformLocation(program, "heightMap");

                gl.activeTexture(gl.TEXTURE1);
                gl.bindTexture(gl.TEXTURE_2D, displacementTexture);
                gl.uniform1i(displacementTextureLocation, 1);

            }

        }

    };

    onMount(() => {
        let frame;

        console.log(webgl);

        if (!!displacementTexture === false) {
            // Create a texture and create initial bind
            displacementTexture = webgl.createTexture();
            webgl.bindTexture(webgl.TEXTURE_2D, displacementTexture);
            webgl.bindTexture(webgl.TEXTURE_2D, null);
        }

        // Texture constants
        const level = 0;
        const internalFormat = webgl.RGBA;
        const format = webgl.RGBA;
        const type = webgl.UNSIGNED_BYTE;

        heightMap.addEventListener('load', function () {
            // Now that the image has loaded copy it to the texture.
            console.log("Bind to texture");

            webgl.bindTexture(webgl.TEXTURE_2D, displacementTexture);
            webgl.texImage2D(webgl.TEXTURE_2D, level, internalFormat, format, type, heightMap);
            webgl.generateMipmap(webgl.TEXTURE_2D);
            webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_MAG_FILTER, webgl.NEAREST_MIPMAP_LINEAR);
            webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_MIN_FILTER, webgl.NEAREST_MIPMAP_LINEAR);
        });

        heightMap.src = "images/heightmap.png";

        terrain = new GL.Texture("/images/heightmap.png", { width: 512, height: 512 });

        const loop = () => {
            frame = requestAnimationFrame(loop);

            light.x = 3 * Math.sin(Date.now() * 0.001);
            light.y = 2.5 + 2 * Math.sin(Date.now() * 0.0004);
            light.z = 3 * Math.cos(Date.now() * 0.002);
        };

        loop();

        return () => cancelAnimationFrame(frame);
    });
</script>

<GL.Scene bind:gl={webgl} backgroundOpacity=1.0 process_extra_shader_components={process_extra_shader_components}>
    <GL.Target id="center" location={[0, h/2, 0]}/>

    <GL.OrbitControls maxPolarAngle={Math.PI / 2} let:location>
        <GL.PerspectiveCamera {location} lookAt="center" near={0.01} far={1000}/>
    </GL.OrbitControls>

    <GL.AmbientLight intensity={0.3}/>
    <GL.DirectionalLight direction={[-1,-1,-1]} intensity={0.5}/>

    <!-- ground -->
    <GL.Mesh
      geometry={GL.terrain()}
      location={[0, -0.01, 0]}
      rotation={[-90, 0, 0]}
      scale={h}
      vert={terrainVert}
      uniforms={{ color: 0xffffff, bumpmap: terrain }}
    />

    <GL.Mesh
      geometry={GL.plane()}
      location={[0, h/2 - 0.05, 0]}
      rotation={[-90, 0, 0]}
      scale={h}
      uniforms={{ color: 0x0066ff, alpha: 0.45 }}
      transparent
    />

    <!-- moving light -->
    <GL.Group location={[light.x,light.y,light.z]}>
        <GL.Mesh
                geometry={GL.sphere({ turns: 36, bands: 36 })}
                location={[0,0.2,0]}
                scale={0.1}
                uniforms={{ color: adjustColor(color, h), emissive: adjustColor(color) }}
        />

        <GL.PointLight
                location={[0,0,0]}
                color={adjustColor(color, h)}
                intensity={0.6}
        />
    </GL.Group>
</GL.Scene>

<div class="controls">
    <label>
        <input type="color" style="height: 64px" bind:value={color}>
    </label>

    <label>
        <input type="range" bind:value={h} min={0.5} max={2} step={0.1}><br />
        size ({h})
    </label>
</div>

heightmap.png: heightmap.png

And this is what you see: demo

dancingfrog commented 4 years ago

Actually on further exploration of the capabilities of @sveltejs/gl I have found that all I needed to do to achieve the scene above was to include the bumpmap texture uniform in the vertex shader (no custom uniforms or render-time hooks necessary)...

terrain-vert.glsl:

in vec3 position;
in vec3 normal;

out vec3 v_normal;

#if defined(has_colormap) || defined(has_specularitymap) || defined(has_normalmap) || defined(has_bumpmap)
#define has_textures true
#endif

#ifdef has_textures
in vec2 uv;
out vec2 v_uv;
#endif

#if defined(has_normalmap) || defined(has_bumpmap)
out vec3 v_view_position;
#endif

out vec3 v_surface_to_light[NUM_LIGHTS];

#ifdef has_specularity
out vec3 v_surface_to_view[NUM_LIGHTS];
#endif

#ifdef USE_FOG
out float v_fog_depth;
#endif

// So, standard @sveltejs/gl default shader so far, and then ...

#define NAME terrain-vert

// texture containing elevation data
uniform sampler2D bumpmap;

void main() {
    float displacement = texture(bumpmap, uv).r;

    vec3 displace_along_normal = vec3(normal * displacement);

    vec3 displaced_position = position + (0.99 * displace_along_normal);

    v_normal = (MODEL_INVERSE_TRANSPOSE * vec4(normal, 0.0)).xyz;

    vec4 pos = vec4(displaced_position, 1.0);

    vec4 model_view_pos = VIEW * MODEL * pos;

    #ifdef has_textures
    v_uv = uv;
    #endif

    #if defined(has_normalmap) || defined(has_bumpmap)
    v_view_position = model_view_pos.xyz;
    #endif

    #ifdef USE_FOG
    v_fog_depth = -model_view_pos.z;
    #endif

    for (int i = 0; i < NUM_LIGHTS; i += 1) {
        PointLight light = POINT_LIGHTS[i];

        vec3 surface_world_position = (MODEL * pos).xyz;
        v_surface_to_light[i] = light.location - surface_world_position;

        #ifdef has_specularity
        v_surface_to_view[i] = CAMERA_WORLD_POSITION - surface_world_position;
        #endif
    }

    gl_Position = PROJECTION * model_view_pos;
}

app.velte:

        <script>
            import { onMount } from 'svelte';
            import * as GL from '@sveltejs/gl';
            import terrainVert from './shaders/custom/terrain-vert.glsl';

            export let title;

            export let color = '#F7C77B';

            let w = 1;
            let h = 1;
            let d = 1;

            const light = {};

            function adjustColor (clr, height = 1) {
                const r = parseInt('0x' + clr.substr(1, 2), 16),
                  g = parseInt('0x' + clr.substr(3, 2), 16),
                  b = parseInt('0x' + clr.substr(5, 2), 16);

                const hr = Math.floor(r * (height / 0.25)),
                  hb = Math.floor(b * (height / 0.25));
                return Math.abs((((hr < 255) ? hr : r) << 16) + (g << 8) + ((hb < 255) ? hb : b));
            }

            let webgl;
            let terrain;

            onMount(() => {
                let frame;

                terrain = new GL.Texture("/images/heightmap.png", { width: 512, height: 512 });

                const loop = () => {
                    frame = requestAnimationFrame(loop);

                    light.x = 3 * Math.sin(Date.now() * 0.001);
                    light.y = 2.5 + 2 * Math.sin(Date.now() * 0.0004);
                    light.z = 3 * Math.cos(Date.now() * 0.002);
                };

                loop();

                return () => cancelAnimationFrame(frame);
            });
        </script>

        <GL.Scene bind:gl={webgl} backgroundOpacity=1.0 process_extra_shader_components={null}>
            <GL.Target id="center" location={[0, h/2, 0]}/>

            <GL.OrbitControls maxPolarAngle={Math.PI / 2} let:location>
                <GL.PerspectiveCamera {location} lookAt="center" near={0.01} far={1000}/>
            </GL.OrbitControls>

            <GL.AmbientLight intensity={0.3}/>
            <GL.DirectionalLight direction={[-1,-1,-1]} intensity={0.5}/>

            <!-- ground -->
            <GL.Mesh
              geometry={GL.terrain()}
              location={[0, -0.01, 0]}
              rotation={[-90, 0, 0]}
              scale={h}
              vert={terrainVert}
              uniforms={{ color: adjustColor(color, h), alpha: 1.0, bumpmap: terrain }}
            />

            <GL.Mesh
              geometry={GL.plane()}
              location={[0, h/2 - 0.05, 0]}
              rotation={[-90, 0, 0]}
              scale={h}
              uniforms={{ color: 0x0066ff, alpha: 0.45 }}
              transparent
            />

            <!-- moving light -->
            <GL.Group location={[light.x,light.y,light.z]}>
                <GL.Mesh
                  geometry={GL.sphere({ turns: 36, bands: 36 })}
                  location={[0,0.2,0]}
                  scale={0.1}
                  uniforms={{ color: adjustColor(color, h), emissive: adjustColor(color) }}
                />

                <GL.PointLight
                        location={[0,0,0]}
                        color={adjustColor(color, 1.0)}
                        intensity={0.6}
                />
            </GL.Group>
        </GL.Scene>

        <div class="controls">
            <label>
                <input type="color" style="height: 64px" bind:value={color}>
            </label>

            <label>
                <input type="range" bind:value={h} min={0.5} max={2} step={0.1}><br />
                size ({h})
            </label>
        </div>

The hook could still be useful for binding other inputs to custom shaders, but I guess many effects are possible with what's currently in @sveltejs/gl (although I did have to use the GL.terrain mesh, or else the vertex displacement would not be possible)

dancingfrog commented 4 years ago

... for instance, accessing the normal map texture in the vertex shader and computing a directional lighting model (which doesn't require a very fine resolution):

image