elm-explorations / webgl

Functional rendering with WebGL in Elm
https://package.elm-lang.org/packages/elm-explorations/webgl/latest/
BSD 3-Clause "New" or "Revised" License
117 stars 17 forks source link

Mesh instancing support #15

Open MartinSStewart opened 5 years ago

MartinSStewart commented 5 years ago

Support for instancing, aka being able to draw the same mesh many times with different uniforms, would be very useful for my project.

Here's an example of my current work around to be able to efficiently draw many instances of a single mesh (I've excluded some details but this is the general idea).

type alias ChipTileVertex =
    { position : Vec3
    , instanceIndex : Float
    }

type alias ChipTileUniforms =
    { perspective : Mat4
    , camera : Mat4
    , time : Second
    , instanceOffset0 : Vec3
    , instanceOffset1 : Vec3
    , instanceOffset2 : Vec3
    , instanceOffset3 : Vec3
    , instanceOffset4 : Vec3
    , instanceOffset5 : Vec3
    , instanceOffset6 : Vec3
    , instanceOffset7 : Vec3
    , instanceOffset8 : Vec3
    , instanceOffset9 : Vec3
    , instanceOffset10 : Vec3
    , instanceOffset11 : Vec3
    , instanceOffset12 : Vec3
    , instanceOffset13 : Vec3
    , instanceOffset14 : Vec3
    , instanceOffset15 : Vec3
    , instanceOffset16 : Vec3
    , instanceOffset17 : Vec3
    , instanceOffset18 : Vec3
    , instanceOffset19 : Vec3
    }

chipTileUniforms : InGameModel -> Size Int -> Array Point2d -> ChipTileUniforms
chipTileUniforms inGameModel windowSize particles =
    let
        getOffset index =
            Array.get index particles
                |> Maybe.map
                    (\position ->
                        Vec3.vec3 (Point2d.xCoordinate position) (Point2d.yCoordinate position) 0
                    )
                |> Maybe.withDefault (Vec3.vec3 -999999 -999999 0)
    in
    { perspective = perspective windowSize
    , camera = cameraMatrix inGameModel
    , time = inGameModel.inGameTime
    , instanceOffset0 = getOffset 0
    , instanceOffset1 = getOffset 1
    , instanceOffset2 = getOffset 2
    , instanceOffset3 = getOffset 3
    , instanceOffset4 = getOffset 4
    , instanceOffset5 = getOffset 5
    , instanceOffset6 = getOffset 6
    , instanceOffset7 = getOffset 7
    , instanceOffset8 = getOffset 8
    , instanceOffset9 = getOffset 9
    , instanceOffset10 = getOffset 10
    , instanceOffset11 = getOffset 11
    , instanceOffset12 = getOffset 12
    , instanceOffset13 = getOffset 13
    , instanceOffset14 = getOffset 14
    , instanceOffset15 = getOffset 15
    , instanceOffset16 = getOffset 16
    , instanceOffset17 = getOffset 17
    , instanceOffset18 = getOffset 18
    , instanceOffset19 = getOffset 19
    }

chipTileVS : Shader ChipTileVertex ChipTileUniforms { vcolor : Vec4 }
chipTileVS =
    [glsl|
        attribute vec3 position;
        attribute float instanceIndex;
        uniform mat4 perspective;
        uniform mat4 camera;
        uniform vec3 instanceOffset0;
        uniform vec3 instanceOffset1;
        uniform vec3 instanceOffset2;
        uniform vec3 instanceOffset3;
        uniform vec3 instanceOffset4;
        uniform vec3 instanceOffset5;
        uniform vec3 instanceOffset6;
        uniform vec3 instanceOffset7;
        uniform vec3 instanceOffset8;
        uniform vec3 instanceOffset9;
        uniform vec3 instanceOffset10;
        uniform vec3 instanceOffset11;
        uniform vec3 instanceOffset12;
        uniform vec3 instanceOffset13;
        uniform vec3 instanceOffset14;
        uniform vec3 instanceOffset15;
        uniform vec3 instanceOffset16;
        uniform vec3 instanceOffset17;
        uniform vec3 instanceOffset18;
        uniform vec3 instanceOffset19;
        uniform float time;
        varying vec4 vcolor;

        void main () {
            mediump int index = int(instanceIndex);
            mediump vec3 offset =
                (instanceOffset0 * float(index == 0)) +
                (instanceOffset1 * float(index == 1)) +
                (instanceOffset2 * float(index == 2)) +
                (instanceOffset3 * float(index == 3)) +
                (instanceOffset4 * float(index == 4)) +
                (instanceOffset5 * float(index == 5)) +
                (instanceOffset6 * float(index == 6)) +
                (instanceOffset7 * float(index == 7)) +
                (instanceOffset8 * float(index == 8)) +
                (instanceOffset9 * float(index == 9)) +
                (instanceOffset10 * float(index == 10)) +
                (instanceOffset11 * float(index == 11)) +
                (instanceOffset12 * float(index == 12)) +
                (instanceOffset13 * float(index == 13)) +
                (instanceOffset14 * float(index == 14)) +
                (instanceOffset15 * float(index == 15)) +
                (instanceOffset16 * float(index == 16)) +
                (instanceOffset17 * float(index == 17)) +
                (instanceOffset18 * float(index == 18)) +
                (instanceOffset19 * float(index == 19));

            gl_Position = perspective * camera * vec4(position + vec3(offset.xy, 0.0), 1.0);
            vcolor = vec4(0.0, 0.5, 0.0, 0.5);
        }
    |]

It's very repetative code and if I want a new shader that also draws multiple instances, I need to copy paste this code.

Also while it's faster than making one draw call per instance, having to do the following in the vertex shader is probably expensive.

        mediump vec3 offset =
                (instanceOffset0 * float(index == 0)) +
                (instanceOffset1 * float(index == 1)) +
                (instanceOffset2 * float(index == 2)) +
                (instanceOffset3 * float(index == 3)) +
                (instanceOffset4 * float(index == 4)) +
                (instanceOffset5 * float(index == 5)) +
                (instanceOffset6 * float(index == 6)) +
                (instanceOffset7 * float(index == 7)) +
                (instanceOffset8 * float(index == 8)) +
                (instanceOffset9 * float(index == 9)) +
                (instanceOffset10 * float(index == 10)) +
                (instanceOffset11 * float(index == 11)) +
                (instanceOffset12 * float(index == 12)) +
                (instanceOffset13 * float(index == 13)) +
                (instanceOffset14 * float(index == 14)) +
                (instanceOffset15 * float(index == 15)) +
                (instanceOffset16 * float(index == 16)) +
                (instanceOffset17 * float(index == 17)) +
                (instanceOffset18 * float(index == 18)) +
                (instanceOffset19 * float(index == 19));

I admit, I haven't entirely thought out how the elm-webgl API should be modified in order to support instancing. My initial idea is to change the uniform parameter in entity to a list. I'm happy to discuss how this might be organized.

MartinSStewart commented 5 years ago

I've done some research. It looks like WebGL 1* only supports instancing via ANGLE_instanced_arrays. Unfortunately this doesn't suppprt my idea of having an array of uniform groups and running the vertex shader on all the attributes once for each group of uniforms. As I understand it, you instead include an additional attribute array that only advances after all of the "normal" attributes have been used by the vertex shader.

Since we need to use attribute arrays instead of an array of uniforms groups to get instancing to work, my guess is that a new function and new shader type will be needed. Something like this:

manyEntities : 
    InstanceShader attributes perInstanceAttributes uniforms varyings
    -> Shader {} uniforms varyings
    -> Mesh attributes
    -> List perInstanceAttributes -- List.length here determines how many instances we draw. Also unlike attributes, perInstanceAttributes will change often.
    -> uniforms -- Same data is used for all instances
    -> Entity

I imagine adding a new shader type is a substantial change. Maybe even requires changes to the compiler. I think it's worthwhile but I imagine it won't be added anytime soon.

I haven't worked with WebGL much so someone let me know if I'm totally off track or if there's a better solution.

*I'm assuming this package targets WebGL 1 because version 2 isn't supported on Safari or Edge yet.

w0rm commented 5 years ago

@MartinSStewart thanks for investigating this use case! I'm not exactly sure if we should add custom api for each custom use case like this, or we should rather model WebGL api to be lower level than it is now.

By the way, there is a post on discourse about rendering particles. I linked to this issue from there.

MartinSStewart commented 5 years ago

Good point. Thanks for the link!

David-Klemenc commented 1 year ago

Both methods for instanced drawing:

are well supported now, maybe something like WebGL.entityInstanced could be added?

w0rm commented 12 months ago

@David-Klemenc sorry for the late reply. The functions that you linked to WebGL2, whilst this package only supports WebGL.