Search code examples
javascriptcolorstrianglewebgpuwgsl

WebGPU. How to draw the Triangle Strip with different color for each Triangle?


The first triangle must be RED, the second triangle must be GREEN.

But both are GREEN (((

How to set different colors for each sub-triangle in 'triangle-strip' primitive topology ?

My final goal is: Creating new triangles with new colors at mouse click point in 'triangle-strip' mode.

This is my code for 2 triangles for simplicity:

body{ background-color: #000 }
canvas{ display: block; width: 600px; height: 400px; outline: 1px solid #666 }
<canvas width=900 height=600></canvas>
<script type="module">
let C = document.querySelector('canvas').getContext(`webgpu`),

code=`
var<private> fi: i32; // fragment_index ( current triangle )

@vertex
fn vs( @builtin(vertex_index) vi: u32 ) -> @builtin(position) vec4f {
  if(vi<3){ fi = 1;
    var T = array<vec2f, 3>( vec2f(0,0), vec2f(.4,.7), vec2f(.8,0) );
    return vec4f(T[vi],0,1);
  };
  fi = 2;
  return vec4f(.6,-.5,0,1);
}

@fragment
fn fs() -> @location(0) vec4f {
  if(fi == 1){ return vec4f(.7,.2,.2,.5); }; // color for 1st triangle 🔺
  return vec4f(.3,.6,.4,.5);                 // color for 2nd triangle
}`,

 format = `bgra8unorm`,
adapter = await navigator.gpu.requestAdapter(),
 device = await adapter.requestDevice(),
      Q = device.queue,
      A = {loadOp: `clear`, storeOp: `store`}, // Attachments
      O = {colorAttachments: [ A ]},           // Render Pass Descriptor
      E, R,
 module = device.createShaderModule({ code }),
      P = device.createRenderPipeline({ layout: `auto`, primitive: { topology: `triangle-strip` },
             vertex: { module, entryPoint: `vs`, },
           fragment: { module, entryPoint: `fs`, targets: [{ format }] }
          });

C.configure({ device, format });

function F(){
  A.view = C.getCurrentTexture().createView();
  
  E = device.createCommandEncoder();
  R = E.beginRenderPass(O);
  R.setPipeline(P);
  
  R.draw(4);
  R.end();
  Q.submit([E.finish()]);
  
  requestAnimationFrame(F)
}

F()

</script>


Solution

  • I don't think var<private> fi: i32 is doing what you think it's doing.

    vertex shaders and fragment shaders share nothing. You can consider them entirely separate. Your vertex shader is

    var<private> fi: i32;
    
    @vertex
    fn vs(...) -> ...
    

    and your fragment shader is

    var<private> fi: i32;
    
    @fragment
    fn fs(...) -> ...
    

    The fi variable is not shared.

    To pass data from a vertex shader to a fragment shader you need to use inter-stage variables

    So, below I changed fi to an inter-stage variable by putting it a struct, returning that struct from the vertex shader. It's important to note, just like I said above, the fragment shader and vertex shader are not sharing the actual data in the struct. The data is connected by @location(0) not by the struct itself.

    Further, inter-stage variables normally get interpolated. You can turn off the interpolation by adding @interpolate(flat) in which case, the value passed to the fragment shader will be the value of the 1st vertex of the triangle. In this case the first vertex of the first triangle is vertex 0. The first vertex of the 2nd triangle is vertex 1

    body{ background-color: #000 }
    canvas{ display: block; width: 600px; height: 400px; outline: 1px solid #666 }
    <canvas width=900 height=600></canvas>
    <script type="module">
    let C = document.querySelector('canvas').getContext(`webgpu`),
    
    code=`
    
    struct VSOut {
      @builtin(position) pos: vec4f,
      @location(0) @interpolate(flat) fi: i32,
    };
    
    @vertex
    fn vs( @builtin(vertex_index) vi: u32 ) -> VSOut {
    
      // inter-stage variables are interpolated. In flat interpolation mode,
      // the values passed to the fragment shader are from the "provoking vertex"
      // which is the value set on the 1st vertex of the triangle
      var vsOut: VSOut;
      vsOut.fi = 1;
      if (vi > 0) {
        vsOut.fi = 2;
      }
    
      if(vi<3){
        var T = array<vec2f, 3>( vec2f(0,0), vec2f(.4,.7), vec2f(.8,0) );
        vsOut.pos = vec4f(T[vi],0,1);
        return vsOut;
      };
      vsOut.pos = vec4f(.6,-.5,0,1);
      return vsOut;
    }
    
    @fragment
    fn fs(vsOut: VSOut) -> @location(0) vec4f {
      if(vsOut.fi == 1){ return vec4f(.7,.2,.2,.5); }; // color for 1st triangle 🔺
      return vec4f(.3,.6,.4,.5);                 // color for 2nd triangle
    }`,
    
     format = `bgra8unorm`,
    adapter = await navigator.gpu.requestAdapter(),
     device = await adapter.requestDevice(),
          Q = device.queue,
          A = {loadOp: `clear`, storeOp: `store`}, // Attachments
          O = {colorAttachments: [ A ]},           // Render Pass Descriptor
          E, R,
     module = device.createShaderModule({ code }),
          P = device.createRenderPipeline({ layout: `auto`, primitive: { topology: `triangle-strip` },
                 vertex: { module, entryPoint: `vs`, },
               fragment: { module, entryPoint: `fs`, targets: [{ format }] }
              });
    
    C.configure({ device, format });
    
    function F(){
      A.view = C.getCurrentTexture().createView();
      
      E = device.createCommandEncoder();
      R = E.beginRenderPass(O);
      R.setPipeline(P);
      
      R.draw(4);
      R.end();
      Q.submit([E.finish()]);
      
      requestAnimationFrame(F)
    }
    
    F()
    
    </script>

    with blending

    body{ background-color: #000 }
    canvas{ display: block; width: 600px; height: 400px; outline: 1px solid #666 }
    <canvas width=900 height=600></canvas>
    <script type="module">
    let C = document.querySelector('canvas').getContext(`webgpu`),
    
    code=`
    
    struct VSOut {
      @builtin(position) pos: vec4f,
      @location(0) @interpolate(flat) fi: i32,
    };
    
    @vertex
    fn vs( @builtin(vertex_index) vi: u32 ) -> VSOut {
    
      // inter-stage variables are interpolated. In flat interpolation mode,
      // the values passed to the fragment shader are from the "provoking vertex"
      // which is the value set on the 1st vertex of the triangle
      var vsOut: VSOut;
      vsOut.fi = 1;
      if (vi > 0) {
        vsOut.fi = 2;
      }
    
      if(vi<3){
        var T = array<vec2f, 3>( vec2f(0,0), vec2f(.4,.7), vec2f(.8,0) );
        vsOut.pos = vec4f(T[vi],0,1);
        return vsOut;
      };
      vsOut.pos = vec4f(.6,-.5,0,1);
      return vsOut;
    }
    
    @fragment
    fn fs(vsOut: VSOut) -> @location(0) vec4f {
      if(vsOut.fi == 1){ return vec4f(.7,.2,.2,.5); }; // color for 1st triangle 🔺
      return vec4f(.3,.6,.4,.5);                 // color for 2nd triangle
    }`,
    
     format = `bgra8unorm`,
    adapter = await navigator.gpu.requestAdapter(),
     device = await adapter.requestDevice(),
          Q = device.queue,
          A = {loadOp: `clear`, storeOp: `store`}, // Attachments
          O = {colorAttachments: [ A ]},           // Render Pass Descriptor
          E, R,
     module = device.createShaderModule({ code }),
          P = device.createRenderPipeline({ layout: `auto`, primitive: { topology: `triangle-strip` },
                 vertex: { module, entryPoint: `vs`, },
               fragment: { module, entryPoint: `fs`, targets: [{ format,           blend: {
                color: {
                  srcFactor: 'one',
                  dstFactor: 'one-minus-src-alpha',
                  operation: 'add',
                },
                alpha: {
                  srcFactor: 'one',
                  dstFactor: 'one-minus-src-alpha',
                  operation: 'add',
                },
              }, }] }
              });
    
    C.configure({ device, format });
    
    function F(){
      A.view = C.getCurrentTexture().createView();
      
      E = device.createCommandEncoder();
      R = E.beginRenderPass(O);
      R.setPipeline(P);
      
      R.draw(4);
      R.end();
      Q.submit([E.finish()]);
      
      requestAnimationFrame(F)
    }
    
    F()
    
    </script>