www.xbdev.net
xbdev - software development
Saturday June 13, 2026
Home | Contact | Support | WebGPU Graphics and Compute ... | WebGPU.. Games, Tutorials, Demos, Projects, and Code.....
     
 

WebGPU..

Games, Tutorials, Demos, Projects, and Code.....

 


Multi-Texturing


We're now starting to open pandoras box of textures! As multiple textures mean you can combine textures - both loaded ones and generated ones - you can also mix transforms so the different textures are manimpulated in different ways.

These textures are eventually combined to create an infinite number of possibilities. For example, loading in a brick surface and mixing in dirt and scratches on top.


Simple multitexturing example - combining two textures together to create a new result.
Simple multitexturing example - combining two textures together to create a new result.


Functions Used: getContext(), requestAdapter(), getPreferredCanvasFormat(), createCommandEncoder(), beginRenderPass(), setPipeline(), draw(), end(), submit(), getCurrentTexture(), createView(), createShaderModule()

The simple fragment shader below has two textures (myTexture0 and myTexture1). We use the first texture as the main color and the second texture contains some scratches and noise that we want to overlay.

We scale and clamp the noise texture and use only one of the components - the texture is black and white (so we only need the red component);


@group(0) @binding(1) var mySampler: sampler;
@group(0) @binding(2) var myTexture0: texture_2d<f32>;
@group(0) @binding(3) var<uniform> myTimer  : f32;
@group(0) @binding(4) var myTexture1: texture_2d<f32>;

@fragment 
fn psmain(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> 
{    

    var texCol0 = textureSample(myTexture0, mySampler, uv ).xyz;
    
    var texCol1 = textureSample(myTexture1, mySampler, uv ).xyz;
    
    var col = texCol0 * clamp(texCol1.r*1.5, 0.0, 1.0);
    
    return vec4<f32>( col, 1.0 ); // set alpha to 1.0 so there isn't any transparncy

}



Example shows the original texture and a scratch texture which are combined to produce a scratched old texture.
Example shows the original texture and a scratch texture which are combined to produce a scratched old texture.



Textures can come in many formats - for 32bit rgba and single 8bit gray scale - this can be accomodated for in the file and the shader so your loading and rendering is more efficient (just default to 32bit so it's easier for the examples).


The full code for the example is given below.

// Load matrix library on dynamically (on-the-fly)
let matprom = await fetch( 'https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.6.0/gl-matrix-min.js' );
let mattex  = await matprom.text();
var script   = document.createElement('script');
script.type  = 'text/javascript';
script.innerHTML = mattex;
document.head.appendChild(script); 

// -------------
let canvas = document.createElement('canvas');
document.body.appendChild( canvas ); canvas.height=canvas.width=512;

const context = canvas.getContext('webgpu');
const adapter = await navigator.gpu.requestAdapter();
const device  = await adapter.requestDevice();
const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); 
context.configure({ device: device, format: presentationFormat  });

async function loadTexture( fileName = "https://webgpulab.xbdev.net/var/images/test512.png" )
{
  console.log('loading image:', fileName );
  // Load image 
  const img = document.createElement("img");
  img.src = fileName;

  await Promise.all([
    img.decode()
  ]);

  let imgWidth  = img.width;
  let imgHeight = img.height;

  const imageCanvas = document.createElement('canvas');
  imageCanvas.width =  imgWidth;
  imageCanvas.height = imgHeight;
  const imageCanvasContext = imageCanvas.getContext('2d');
  imageCanvasContext.drawImage(img, 0, 0, imgWidth, imgHeight);
  const imageData = imageCanvasContext.getImageData(0, 0, imgWidth, imgHeight);
  let textureData= imageData.data;
  console.log('textureData.byteLength:', textureData.byteLength );

  // Create a texture and a sampler using WebGPU
  const sampler = device.createSampler({
    minFilter: "linear",
    magFilter: "linear"  
  });

  const basicTexture = device.createTexture({
    size: [imgWidth, imgHeight, 1],
    format: "rgba8unorm",
    usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING
  });

  await
  device.queue.writeTexture(
      { texture:basicTexture },
      textureData,
      { bytesPerRow: imgWidth * 4 },
      [ imgWidth, imgHeight, 1 ]
  );
  return { w:imgWidth, h:imgHeight, s:sampler, t:basicTexture };
}// end loadTexture(..)

function createTexturedSquare( device )
{
  let positionVertex = new Float32Array([
     1.0,    1.0,   0.0,
    -1.0,    1.0,   0.0,
     1.0,   -1.0,   0.0,
    -1.0,   -1.0,   0.0
  ]);
  const vBuffer = device.createBuffer({ size:  positionVertex.byteLength,
                                        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
  device.queue.writeBuffer(vBuffer, 0, positionVertex);
  
  let uvVertex = new Float32Array([
     1.0,   0.0,
     0.0,   0.0,
     1.0,   1.0,
     0.0,   1.0,
  ]);
  const uvBuffer = device.createBuffer({ size:  uvVertex.byteLength,
                                        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
  device.queue.writeBuffer(uvBuffer, 0, uvVertex);
  
  // return the vertex and texture buffers
  return { v:vBuffer, t:uvBuffer };
}

function createMatrixUniform( )
{
  // Create the matrix in Javascript (using matrix library)
  const projectionMatrix     = mat4.create();
  const viewMatrix           = mat4.create();
  const viewProjectionMatrix = mat4.create();
  
  mat4.perspective(projectionMatrix, Math.PI / 2, canvas.width / canvas.height, 0.001, 500.0)
  mat4.lookAt(viewMatrix, [0, 0, 1.5],  [0, 0, 0], [0, 1, 0]);
  mat4.multiply(viewProjectionMatrix, projectionMatrix, viewMatrix);
  
  // Create a buffer using WebGPU API (copy matrix into it)
  const matrixUniformBuffer = device.createBuffer({
     size: viewProjectionMatrix.byteLength ,
     usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
  });
  device.queue.writeBuffer(matrixUniformBuffer, 0, viewProjectionMatrix );

  return matrixUniformBuffer;
}

// Create a timer for the shader - pass timer
let myTimer = new Float32Array( [0.0] );
const timerBuffer = device.createBuffer({size: myTimer.byteLength, 
                                                 usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });


let shaderWGSL = `
@group(0) @binding(0) var<uniform> viewProjectionmMatrix : mat4x4<f32>;

struct vsout {
    @builtin(position) Position: vec4<f32>,
    @location(0)       uvs     : vec2<f32>
};

@vertex 
fn vsmain(@location(0) pos : vec3<f32>,
          @location(1) uvs : vec2<f32>) -> vsout
{ 
    var r:vsout;
    r.Position = viewProjectionmMatrix * vec4<f32>(pos, 1.0);
    r.uvs      = uvs;
    return r;
}

@group(0) @binding(1) var mySampler: sampler;
@group(0) @binding(2) var myTexture0: texture_2d<f32>;
@group(0) @binding(3) var<uniform> myTimer  : f32;
@group(0) @binding(4) var myTexture1: texture_2d<f32>;

@fragment 
fn psmain(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> 
{    

    var texCol0 = textureSample(myTexture0, mySampler, uv ).xyz;
    
    var texCol1 = textureSample(myTexture1, mySampler, uv ).xyz;
    
    var col = texCol0 * clamp(texCol1.r*1.5, 0.0, 1.0);
    
    return vec4<f32>( col, 1.0 ); // set alpha to 1.0 so there isn't any transparncy

}`;

const texture0         = await loadTexture( 'https://webgpulab.xbdev.net/var/images/test512.png' );
const texture1         = await loadTexture( 'https://webgpulab.xbdev.net/var/images/cracks.jpg');

const squareBuffer        = createTexturedSquare( device );
const matrixUniformBuffer = createMatrixUniform();
const shaderModule        = device.createShaderModule({ code : shaderWGSL });

// Define the layout information for the shader (uniforms)
const sceneUniformBindGroupLayout = device.createBindGroupLayout({
  entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" }      },
            { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering"  } },
            { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "2d"} },
            { binding: 3, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" }      },
            { binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "2d"} },
           ]
});

const sceneUniformBindGroup = device.createBindGroup({
  layout: sceneUniformBindGroupLayout,
  entries: [{ binding:  0, resource: { buffer: matrixUniformBuffer }    },
            { binding : 1, resource: texture0.s                  },
            { binding : 2, resource: texture0.t.createView()     },
            { binding:  3, resource: { buffer: timerBuffer  }    },
            { binding : 4, resource: texture1.t.createView()     },
           ]
});

const pipeline = device.createRenderPipeline({
  layout: device.createPipelineLayout({bindGroupLayouts: [sceneUniformBindGroupLayout]}),
  vertex:      {   module: shaderModule, entryPoint: 'vsmain', 
                   buffers: [
                            { arrayStride: 4*3,attributes: [ {shaderLocation: 0, offset: 0, format: 'float32x3' } ] },
                            { arrayStride: 4*2,attributes: [ {shaderLocation: 1, offset: 0, format: 'float32x2' } ] }
                            ]
               },
  fragment:    {   module: shaderModule, entryPoint: 'psmain',
                   targets: [ { format: presentationFormat } ]
               }, 
  primitive:   {   topology: 'triangle-strip' },
});

function draw() 
{
  myTimer[0] = myTimer[0] + 0.1;
  device.queue.writeBuffer(timerBuffer, 0, myTimer );
  
  const commandEncoder = device.createCommandEncoder();
  const renderPassDescriptor =                                       { // GPURenderPassDescriptor 
             colorAttachments: [ { view       : context.getCurrentTexture().createView(),
                                   loadOp     : "clear", 
                                   clearValue: [0.8, 0.8, 0.8, 1], // clear screen to color/rgba
                                   storeOp   : 'store' } ]           };
  const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
  passEncoder.setViewport(0.0,  0.0,                   // x, y
                          canvas.width, canvas.height, // width, height
                          0, 1 );                      // minDepth, maxDepth                  
  passEncoder.setPipeline(pipeline);
  passEncoder.setVertexBuffer(0, squareBuffer.v);
  passEncoder.setVertexBuffer(1, squareBuffer.t);
  passEncoder.setBindGroup(0, sceneUniformBindGroup);
  passEncoder.draw(4, 1, 0, 0);
  passEncoder.end();
  device.queue.submit([commandEncoder.finish()]);
  requestAnimationFrame(draw);
}
draw();
console.log('done...');


The example just combines two textures in the simplest way - as discussed in previous tutorials - textures can be manipulated in many different ways (texture transforms) so the combined result can be very unique.


Things to Try


• Adding a 3rd texture
• Use transforms to manipulate the uv coordinates for one of the textures (rotated and stretched)
• Modify the texture `sampler` so it supports texture repeating and mirroring (so combined textures can be tiled instead of just clamping to the last pixel)



Resources and Links


• WebGPU Lab Example [LINK]

























































WebGPU by Example: Fractals, Image Effects, Ray-Tracing, Procedural Geometry, 2D/3D, Particles, Simulations WebGPU Compute graphics and animations using the webgpu api 12 week course kenwright learn webgpu api kenwright programming compute and graphics applications with html5 and webgpu api kenwright real-time 3d graphics with webgpu kenwright webgpu api develompent a quick start guide kenwright webgpu by example 2022 kenwright webgpu gems kenwright webgpu interactive compute and graphics visualization cookbook kenwright wgsl webgpu shading language cookbook kenwright wgsl webgpugems shading language cookbook kenwright



 
Advert (Support Website)

 
 Visitor:
Copyright (c) 2002-2026 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.