www.xbdev.net
xbdev - software development
Tuesday October 15, 2024
Home | Contact | Support | LearnWebGPU Graphics and Compute ...
     
 

LearnWebGPU

Graphics and Compute ...

 

Animated Character Instancing (MD2)


50 thousand animated characters all jumping and moving in real-time on the screen in a web-browser using instancing with the WebGPU API.

The .md2 format is a bit old now, but it does the job, it stores the vertices for each frame - and is interpolated (morphed) to create the animation effect.

The full md2 mesh loading code is included below - simple and sweet! Essentially, it's just a header section whilc include offsets to the mesh data (e.g., vertices, faces, texture coordinates etc).

Once the data is read in and the vertex/index buffers are setup for the WebGPU API! You're ready to go! Render a single instance! If you can render 1, you can render as many as you want.

To manage the instances - so they aren't all drawn on top of one another - you can use the
@builtin(instance_index)
variable in the WGSL shader - so that each instance is drawn at a differnet location and the frame number for the animation is offset (so they each animate/move differently).


animated md2 characters instanced 50 thousand of them


Model Loader/Parser


The following code loads the 3d model data from the md2 file - while the md2 file is a bit old - it's also very compact and easy to implement. Essentially each animation frame stores all the vertices.

Then for each frame the previous and next vertex values (for the position) are interpolated. Typically, the md2 file contains 192ish animation frame - which form idling, walking, jumping and so on.

<script type='module'>
// Function to load a binary file
function loadBinaryFile(url) {
    return 
fetch(url)
        .
then(response => response.arrayBuffer());
}

// Function to parse the MD2 file
function parseMD2(buffer) {
    const 
dataView = new DataView(buffer);

    const 
header = {
        
identdataView.getUint32(0true),
        
versiondataView.getUint32(4true),
        
skinWidthdataView.getUint32(8true),
        
skinHeightdataView.getUint32(12true),
        
frameSizedataView.getUint32(16true),
        
numSkinsdataView.getUint32(20true),
        
numVerticesdataView.getUint32(24true),
        
numTexCoordsdataView.getUint32(28true),
        
numTrianglesdataView.getUint32(32true),
        
numGlCommandsdataView.getUint32(36true),
        
numFramesdataView.getUint32(40true),
        
offsetSkinsdataView.getUint32(44true),
        
offsetTexCoordsdataView.getUint32(48true),
        
offsetTrianglesdataView.getUint32(52true),
        
offsetFramesdataView.getUint32(56true),
        
offsetGlCommandsdataView.getUint32(60true),
        
offsetEnddataView.getUint32(64true)
    };

    
// Ensure the file is a valid MD2 file
    
if (header.ident !== 844121161 || header.version !== 8) {
        throw new 
Error("Not a valid MD2 file");
    }

      
console.log('header.offsetTriangles:'header.offsetTriangles );
  
     
let faces = [];
    
let offset header.offsetTriangles;
    for (
let i 0header.numTrianglesi++)
    {
        
let i0 dataView.getUint16(offsettrue);
        
let i1 dataView.getUint16(offset+2true);
        
let i2 dataView.getUint16(offset+4true);
          
faces.pushi0i1i2 );
        
offset += 12;
    }
    
faces faces.flat(3);
      
    
// Extract frame data
    
const frames = [];
    
offset header.offsetFrames;

    for (
let i 0header.numFramesi++) {
        const 
frame = {
            
scale: [
                
dataView.getFloat32(offsettrue),
                
dataView.getFloat32(offset 4true),
                
dataView.getFloat32(offset 8true)
            ],
            
translate: [
                
dataView.getFloat32(offset 12true),
                
dataView.getFloat32(offset 16true),
                
dataView.getFloat32(offset 20true)
            ],
            
name'',
            
vertices: []
        };

        
offset += 24;
      
        const 
nameBytes = new Uint8Array(bufferoffset16);
        
frame.name String.fromCharCode(...nameBytes).replace(/\0/g'');
      
        
offset += 16;

        for (
let j 0header.numVerticesj++) {
            
let x dataView.getUint8(offset) * frame.scale[0] + frame.translate[0];
            
let y dataView.getUint8(offset 1) * frame.scale[1] + frame.translate[1];
            
let z dataView.getUint8(offset 2) * frame.scale[2] + frame.translate[2];
          
            const 
vertex = {
                
pos: [
                    
xz, -// swizzle xyz => xz(-y)
                
],
                
normalIndexdataView.getUint8(offset 3)
            };
            
frame.vertices.push(vertex);
            
offset += 4;
        }

        
frames.push(frame);
    }
    
  
    return {
        
header,
        
frames,
        
faces
    
};
}


// Example usage
// xbdev.net
let buffer2 await loadBinaryFile('https://webgpulab.xbdev.net/var/resources/pac3D.md2');
const 
model parseMD2(buffer2);


console.log('header:'model.header );
console.log('number frames:'model.frames.length );
console.log('number frames:'Object.keys(model.frames[0]) );
console.log('number frames[0] name:'model.frames[0].name );  
console.log('number frames[0] pos:'model.frames[0].vertices[0].pos );
console.log('number frames[0] normalIndex:'model.frames[0].vertices[0].normalIndex );


Example output for the console log:

log:["header.offsetTriangles:",1564]
log:["header:",{"ident":844121161,"version":8,"skinWidth":256,"skinHeight":256,
                
"frameSize":936,"numSkins":0,"numVertices":224,"numTexCoords":374,
                
"numTriangles":416,"numGlCommands":2089,"numFrames":198,"offsetSkins":68,
                
"offsetTexCoords":68,"offsetTriangles":1564,"offsetFrames":6556,
                
"offsetGlCommands":191884,"offsetEnd":200240}]
log:["number frames:",198]
log:["number frames:",["scale","translate","name","vertices"]]
log:["number frames[0] name:","stand01"]
log:["number frames[0] pos:",[-2.645264983177185,17.05917051434517,10.30500990152359]]
log:["number frames[0] normalIndex:",121]


Instancing



The following is the full vertex fragment shader for the implementation. The vertex animation frames are stored in the memory variable
frames
.



Outpu render (md2 models) - single instance, wireframe and solid 50,000 instance.
Outpu render (md2 models) - single instance, wireframe and solid 50,000 instance.



struct Uniforms {
  
modelViewProjectionMatrix mat4x4<f32>,
};
@
binding(0) @group(0) var<uniformuniforms Uniforms;
@
binding(1) @group(0) var<storage,readframes  : array< f32 >;  
@
binding(2) @group(0) var<uniformmytimer f32;    
@
binding(5) @group(0) var<storage,readcoords    : array< f32 >; 
@
binding(6) @group(0) var<storage,readtexfaces  : array< u32 >; 

struct VSOut {
    @
builtin(positionPositionvec4<f32>,
    @
location(0)       pos     vec3<f32>,
    @
location(1)       uvs     vec2<f32>,
};

fn 
random(uvvec2<f32>) -> f32 {
  return 
fract(sin(dot(uvvec2<f32>(12.989878.233))) * 43758.5453);
}
  
@
vertex
fn main(@builtin(instance_indexinstanceIndexu32,
        @
builtin(vertex_indexvertexIndex u32,
        @
location(0inPos  vec3<f32>,
        @
location(1normal vec3<f32>) -> VSOut 
{
  
      var 
off vec3<f32>(0.0);
      var 
numInst:f32 = ${numInstances};
    var 
numInstSqrt:u32 = ${Math.round(Math.sqrt(numInstances))};
  
    
let ix f32instanceIndex numInstSqrt ) / f32(numInstSqrt);
    
let iz f32instanceIndex numInstSqrt ) / f32(numInstSqrt);
      
let delta = ${sepdist} * numInst;
  
      
off.= -delta 2.0 delta ix;
    
off.= -delta 2.0 delta iz;
  
  
    
let n numInst randomvec2<f32>( f32(instanceIndex) * 0.129303219,
                                         
f32(instanceIndex) * 0.29238321 ) );
  
      
let speed 0.2;
    
let numVertices:u32 = ${(frameverts.length/numFrames)};
    
let numFrames:u32 = ${numFrames};
    
let startFrameIndex0:u32 numVertices * ( u32mytimer*speed ) % numFrames );
    
let startFrameIndex1:u32 numVertices * ( u32mytimer*speed 1.0 ) % numFrames );
    
    
let framePos0 vec3<f32>( framesstartFrameIndex0 vertexIndex*],
                               
framesstartFrameIndex0 vertexIndex*],
                               
framesstartFrameIndex0 vertexIndex*] );
  
      
let framePos1 vec3<f32>( framesstartFrameIndex1 vertexIndex*],
                               
framesstartFrameIndex1 vertexIndex*],
                               
framesstartFrameIndex1 vertexIndex*] );
  
    
let framePos mixframePos0framePos1fractmytimer*speed ) );
  
  
      
let uvsvec2<f32> = vec2<f32>( framesstartFrameIndex0 vertexIndex*],
                                    
framesstartFrameIndex0 vertexIndex*] );
  
    var 
vsOutVSOut;
    
//vsOut.Position = uniforms.modelViewProjectionMatrix * vec4<f32>(inPos , 1.0);
    
vsOut.Position uniforms.modelViewProjectionMatrix vec4<f32>(framePos off 1.0);
    
vsOut.pos      inPos ;
    
vsOut.uvs      uvs ;
    return 
vsOut;
}



Things to Try


• Try loading in other md2 models (see the resources links)
• Plot number instances vs render time on a chart (see how it changes) - it should be pretty 'flat' - until a threshold is reached and the frame rate suddenly just drops (won't crash - just goes slow)
• Display multiple different animated characters (not all the same ones)
• Add a compute shader pass - each character has a simple logic and walks around and avoids bumping into other characters
• Explore implementing a swarm/flocking - see Flocking demo LINK
• Do 'post-processing' screen-space rendering (add ambient occlusion lighting/effects - without the performance hit - even with hundreds of thousands of characters - see SSAO demo LINK
• Add terrain - use a height map algorithm on the GPU to calculate the height - so it doesn't cost anything to do collisions with the terrain for the characters - see height map demo LINK
• Explore other animation formats - with rigid skeletons/skinning - demo show how skinning works LINK


Resources & Links


• Demo 50,000 Instanced Characters (WebGPU Lab) [LINK]

• MD2 Format and Information [LINK]

• Example MD2 Models [LINK]








WebGPU Development Cookbook - coding recipes for all your webgpu needs! WebGPU by Example: Fractals, Image Effects, Ray-Tracing, Procedural Geometry, 2D/3D, Particles, Simulations WebGPU Games WGSL 2d 3d interactive web-based fun learning WebGPU Compute WebGPU API - Owners WebGPU & WGSL Essentials: A Hands-On Approach to Interactive Graphics, Games, 2D Interfaces, 3D Meshes, Animation, Security and Production Kenwright 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 for dummies 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 WebGPU Shader Language Development: Vertex, Fragment, Compute Shaders for Programmers Kenwright wgsl webgpugems shading language cookbook kenwright WGSL Fundamentals book kenwright WebGPU Data Visualization Cookbook kenwright Special Effects Programming with WebGPU kenwright WebGPU Programming Guide: Interactive Graphics and Compute Programming with WebGPU & WGSL kenwright



 
Advert (Support Website)

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