I need help! Transform objects with single drawarrays call

I need to know how to do per object transforms (for animation) with a single drawArrays call - in js, in the shader? And how?

I have a game engine I am intending to use to prototype iOS and Android games with. I started from a webgl lesson and it has been coming along nicely. Get the source here:GitHub - johnnyscott/soft-voxels: Attempt at robust OpenGl ES 2.0 game engine prototype for refactor for Android and iPhone/iPad game development. Runs in webGl enabled browser (chrome, safari, firefox but firefox sucks like everybody knows when it comes to performance) (I’ll also copy pasta it below)

(Use Chrome)
See it here (until my dynamic IP shuffles):
http://208.126.167.117/webgl/soft-voxels/

You fly around with WASD and look around with the mouse. Also you can hit ~ for the console and type ? for a very sparse list of commands ;D.

At first I was doing a call to drawArrays for every object in the scene per frame and found around 100-1000x performance increase by instead doing a single drawArrays call per frame for all objects. I went from having around 2000 objects at 60fps to easily supporting about 100,000+ at 60fps (actually found no limit)

Clearly a single call to drawArrays is the way to go for performance reasons.

I accomplished this by creating a scene graph ‘class’ that builds a vertex array once before the render loop begins (scene.rebuildBuffers()). This function \ traverses the graph recursively and adds each node’s world coords to its local coords and stuffs the result into the end of this massive array (along with texture coords into another array).

The issue is

  1. I am only doing translation (no rotation or scale) - should have a translation matrix per object probably
  2. I have no idea how I ought to do the transformations per object now. Right now rebuilding the buffers for 40k objects takes a couple hundred ms and I can’t do that once per frame and stay at 60fps.

I thought ‘perhaps I only update the objects that have changed’ but still this requires some kind of monkey tricks and it’s in javascript so it’ll be slow doing matrix multiplication and then updating the vertex positions at some offset.

My other thought was is there some way to maybe have another uniform or something in my shader that holds the transform matrices per object and then update that for the object that has changed and do the transform matrix multiplication in the vertex shader.

I am pasting code below or you can see it at github it below so you can see.

THANKS SO MUCH FOR YOUR HELP!!!

(ps: the code is messy still have unused functions from the example I’m cleaning as I go)

<!DOCTYPE html>
<html>

<head>
<title>Soft Voxels</title>
<meta http-equiv=“content-type” content=“text/html; charset=ISO-8859-1”>
<style>
html,body{ overflow:hidden;background:black; cursor:text;}

canvas {
position: absolute;
top: 0;
left: 0;
background:black;
}

.console{
position:absolute;
z-index:1000;
top:-400px;
left:0;
background:transparent;
height:400px;
width:33%;
opacity:0.90;
}
.console span{
display:block;
position:absolute;
top:0px;
bottom:32px;
left:0;
right:0;
width:auto;
height:auto;
background:#343942;
font-family:‘Dina’, ‘Courier New’, ‘Courier’, monospace;
font-size:15px;
color:white;
overflow-y:scroll;
overflow-x:hidden;
}
.console input{
position:absolute;
bottom:3px;
width:auto;
left:0;
right:0;
height:20px;
}
</style>
<script src=“http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js”></script>
<script type=“text/javascript” src=“glMatrix-0.9.5.min.js”></script>
<script type=“text/javascript” src=“webgl-utils.js”></script>
/* star shader
<script id=“shader-fs” type=“x-shader/x-fragment”>
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vTextureCoord;

uniform sampler2D uSampler;


void main(void) {
    vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
 if ( textureColor.a &lt; 0.4 ) discard ;
    gl_FragColor = textureColor;// * vec4(uColor, 1.0);
}

</script>

<script id=“shader-vs” type=“x-shader/x-vertex”>
attribute vec3 aVertexPosition;
attribute vec2 aTextureCoord;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

varying vec2 vTextureCoord;

void main(void) {
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
    vTextureCoord = aTextureCoord;
}

</script>

<script type=“text/javascript”>

var bL = true; // logging on or off?
var aCommandLog = []; // stack of commands entered
var iCommandLogIndex = -1; // position in command stack
// console comamnd names
var CC_CLEAR = 'clear';
var CC_HELP  = '?';

var LOOK_ANGLE_RANGE = 540;
var VIEW_VOLUME_SIZE = 1000;
var KEY_W = 87;
var KEY_A = 65;
var KEY_S = 83;
var KEY_D = 68;

var KEY_TILDE        = 192;
var KEY_ENTER        = 13;
var KEY_UP           = 38;
var KEY_DOWN         = 40;










var gl;
var fFOV = 45.0 // field of view


var consoleLineNumber = 0;


var zoom = -15;

var tilt = 10;
var spin = 0;
var pan = 0;

var runSpeed = 0;
var runStep = 0.001;

var strafeSpeed = 0;
var pX = 0;
var pY =0;
var pZ = 0;

var friction = .0007;





// fit canvas to window
function fit(){ $('canvas').css({width:$(window).width()+'px'});}


function handleKeys() 
{
    
    // RUN FORWARD
    if (currentlyPressedKeys[KEY_W]) { //w
        // Page Down
        zoom += 0.1;
        runSpeed += runStep;
        
    }
    // STRAFE LEFT 
    if (currentlyPressedKeys[KEY_A]) { //a
        // Up cursor key
        pan += 0.1;
        strafeSpeed +=runStep;
    }
    // RUN BACKWARDS
    if (currentlyPressedKeys[KEY_S]) { //s
        // Page Up
        zoom -= 0.1;
        runSpeed -=runStep;
        
    }
    // STRAFE RIGHT 
    if (currentlyPressedKeys[KEY_D]) { //d
        // Down cursor key
        pan -= 0.1;
        strafeSpeed -= runStep;
    }
    // TOGGLE CONSOLE
    if (currentlyPressedKeys[KEY_TILDE] ) { // ~ `
        // Down cursor key
       toggleConsole();
    }
    
    
    
}




// browser console    
function lC(s){ if(bL){console.log(s);}else{}}    

// engine  console
function l(s)
{     
 if(bL){ 
  consoleLineNumber++; var oConsole =   getConsoleLog();       
  oConsole.html( 
   oConsole.html() 
   + consoleLineNumber 
   + ''
   + s 
   + '


);
oConsole[0].scrollTop = oConsole[0].scrollHeight;
}else{}
}
function getConsoleLog(){ return getConsole().find(‘span’); }
function getConsole(){ return ('.console');} function getConsoleInput(){ return (‘.console input’);}
function setConsoleInput(sCmd){ getConsoleInput().val(sCmd); }
function clearConsoleInput(){ getConsoleInput().val(‘’);}
function clearConsole(){ getConsoleLog().html(‘’); }
var bInConsoleToggle = false;
function toggleConsole()
{
if(bInConsoleToggle){return false;}else{}
bInConsoleToggle = true;

    var o = getConsole();
    
    var iTop = 0;
    if(o.offset().top &gt; -1){
        iTop = -o.height();
        getConsoleInput().blur();
    }else{ getConsoleInput().focus(); }
    o.animate(
    {
        top: iTop + 'px'
    }
    ,150
    ,'swing'
    ,function(){ clearConsoleInput(); bInConsoleToggle = false;  }
    );        
}
function consoleSaveCommand(sCmd)
{
  aCommandLog.push(sCmd);
  iCommandLogIndex = aCommandLog.length -1;
}

// run console command and log command and output
function consoleCommand(sCmd) 
{   
 consoleSaveCommand(sCmd);
 clearConsoleInput();
 
 var sOutput = sCmd; 
 switch(sCmd){
 
  case CC_CLEAR: CC_clear(); return /* special case - don't log clear */; break;
  
  case CC_HELP: sOutput += CC_help(); break;
  
  default: sOutput += CC_bad();
 }
 
 l(sOutput);
}

function consoleInputHandler(e)
{
  var iKeyCode = e.keyCode;
  if(iKeyCode == KEY_ENTER){ 
    getConsole();
    consoleCommand(e.srcElement.value);
  }else{}
  
  if(iKeyCode == KEY_UP){ // backup command
    setConsoleInput(aCommandLog[iCommandLogIndex]);
    iCommandLogIndex--; if(iCommsdddddddddandLogIndex &lt; 0 ){iCommandLogIndex = 0; }else{}
    
  }
  if(iKeyCode == KEY_DOWN){ // next command
    setConsoleInput(aCommandLog[iCommandLogIndex]);
    iCommandLogIndex++; if(iCommandLogIndex &gt;aCommandLog.length-1){iCommandLogIndex = aCommandLog.length-1; }else{}
  }
}
// actual commands for engine console
function CC_bad(){ return '

command doesn't exist or command malformed’; }
function CC_clear(){ clearConsole(); return ‘’; }
function CC_help(){ return ’
Commands

  • clear - clear console

  • ? - this help message’; }

    function initGL(canvas) {
    try {
    gl = canvas.getContext(“experimental-webgl”);
    gl.viewportWidth = canvas.width;
    gl.viewportHeight = canvas.height;

     // perspective
      mat4.perspective(fFOV, gl.viewportWidth / gl.viewportHeight, 0.1, VIEW_VOLUME_SIZE, pMatrix);
        
     // when we want transparency we can do the following:
     //   gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
     //   gl.enable(gl.BLEND);
    
     // we moved this so we don't do it every frame 
        gl.enable (gl.DEPTH_TEST);
    } catch (e) {}
    
    if (!gl) {
        alert("Could not initialise WebGL: try get.webgl.org");
    }
    

    }

    function getShader(gl, id) {
    var shaderScript = document.getElementById(id);
    if (!shaderScript) {
    return null;
    }

      var str = "";
      var k = shaderScript.firstChild;
      while (k) {
          if (k.nodeType == 3) {
              str += k.textContent;
          }
          k = k.nextSibling;
      }
    
      var shader;
      if (shaderScript.type == "x-shader/x-fragment") {
          shader = gl.createShader(gl.FRAGMENT_SHADER);
      } else if (shaderScript.type == "x-shader/x-vertex") {
          shader = gl.createShader(gl.VERTEX_SHADER);
      } else {
          return null;
      }
    
      gl.shaderSource(shader, str);
      gl.compileShader(shader);
    
      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
          alert(gl.getShaderInfoLog(shader));
          return null;
      }
    
      return shader;
    

    }

    var shaderProgram;

    function initShaders() {
    var fragmentShader = getShader(gl, “shader-fs”);
    var vertexShader = getShader(gl, “shader-vs”);

      shaderProgram = gl.createProgram();
      gl.attachShader(shaderProgram, vertexShader);
      gl.attachShader(shaderProgram, fragmentShader);
      gl.linkProgram(shaderProgram);
    
      if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
          alert("Could not initialise shaders");
      }
    
      gl.useProgram(shaderProgram);
    
      shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); // gets an integer, on inspection this is 0
      gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
    
      shaderProgram.textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord"); // gets an integer, on inspection this is 1
      gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute);
    
      shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix"); // projection matrix -&gt; clipping and field of view, perspective
      shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix"); // modelview matrix -&gt; puts camera in scene
      shaderProgram.samplerUniform = gl.getUniformLocation(shaderProgram, "uSampler");
    

    }

    function handleLoadedTexture(texture) {
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
    gl.generateMipmap(gl.TEXTURE_2D);

      gl.bindTexture(gl.TEXTURE_2D, null);
    

    }

    var starTexture;

    function initTexture() {
    starTexture = gl.createTexture();
    starTexture.image = new Image();
    starTexture.image.onload = function () {
    handleLoadedTexture(starTexture)
    }

      starTexture.image.src = "grass-billboard.png";
    

    }

    var mvMatrix = mat4.create();
    var mvMatrixStack = [];
    var pMatrix = mat4.create();

    function mvPushMatrix() {
    var copy = mat4.create();
    mat4.set(mvMatrix, copy);
    mvMatrixStack.push(copy);
    }

    function mvPopMatrix() {
    if (mvMatrixStack.length == 0) {
    throw “Invalid popMatrix!”;
    }
    mvMatrix = mvMatrixStack.pop();
    }

    function setMatrixUniforms() {
    gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix);
    gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix);
    }

    function degToRad(degrees) {
    return degrees * Math.PI / 180;
    }

    var currentlyPressedKeys = {};

    function handleKeyDown(event) {
    currentlyPressedKeys[event.keyCode] = true;
    }

    function handleKeyUp(event) {
    currentlyPressedKeys[event.keyCode] = false;
    }

    var starVertexPositionBuffer;
    var starVertexTextureCoordBuffer;

    function initBuffers() {
    starVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, starVertexPositionBuffer); // gives us a pointer into a store

      vertices = scene.aVertexPositions;
      
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); // actually allocates stoe and pushes data up
      starVertexPositionBuffer.itemSize = 3;
      starVertexPositionBuffer.numItems = vertices.length / starVertexPositionBuffer.itemSize;
    
      starVertexTextureCoordBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, starVertexTextureCoordBuffer);
      
      var textureCoords = scene.aTextureCoords;
      
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW);
      starVertexTextureCoordBuffer.itemSize = 2;
      starVertexTextureCoordBuffer.numItems = textureCoords.length/starVertexTextureCoordBuffer.itemSize;
      
      return;
    

    }

    function drawStar() {
    // gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, starTexture);
    gl.uniform1i(shaderProgram.samplerUniform, 0);

      gl.bindBuffer(gl.ARRAY_BUFFER, starVertexTextureCoordBuffer);
      gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, starVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);
    
      gl.bindBuffer(gl.ARRAY_BUFFER, starVertexPositionBuffer);
      gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, starVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
    
      setMatrixUniforms();
      gl.drawArrays(gl.TRIANGLES, 0, starVertexPositionBuffer.numItems);
    

    }

    function Star(startingDistance, rotationSpeed, position) {
    this[‘x’] = position[0];
    this[‘y’] = position[1];
    this[‘z’] = position[2];

      this.angle = 0;
      this.dist = startingDistance;
      this.rotationSpeed = rotationSpeed;
    

    }

    Star.prototype.draw = function () {
    mvPushMatrix();

      // Move to the star's positionw  
      mat4.translate(mvMatrix, [this.x, this.y, this.z]);
    
      // Draw the star in its main color
      drawStar()
    
      mvPopMatrix();
    

    };

    var effectiveFPMS = 60 / 1000;
    Star.prototype.animate = function (elapsedTime) {

    };

    var stars = [];

    // random half negative
    function rndHN(iRange){
    return (Math.random() * iRange) - iRange/2;
    }

    var scene = null;
    // star constructor: function Star(startingDistance, rotationSpeed) {
    function initWorldObjects() {
    scene = new sceneGraph();
    var numBushes = 38530;
    for (var i=0; i < numBushes; i++) {
    scene.addChild(
    new sceneNode(
    new obj_quad({ position:
    [rndHN(133)
    ,rndHN(1.1)
    ,rndHN(203)]
    })
    )
    , 0);

      }
     var i = new Date().getTime();
      scene.rebuildBuffers(); // generate nice arrays of vertices
      // array concat is slow  that's the problem traversing only takes like 4 ms
        console.log(new Date().getTime() - i);
    

    }

    function drawScene() {
    // is this needed? I commented it out and behavior is the same ??
    //gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);

      gl.clear(gl.COLOR_BUFFER_BIT );
    
      // THIS IS DONE IN INIT NOW since it need only be done once
      //    mat4.perspective(fFOV, gl.viewportWidth / gl.viewportHeight, 0.1, VIEW_VOLUME_SIZE, pMatrix);
      
    
    
      mat4.identity(mvMatrix);
      
      mat4.rotate(mvMatrix, degToRad(oMouse.angleY), [1.0, 0.0, 0.0]);
      mat4.rotate(mvMatrix, degToRad(oMouse.angleX), [0.0, 1.0, 0.0]);
      mat4.translate(mvMatrix, [pX, pY, pZ]);
    
      mvPushMatrix();
    
      // Move to the star's positionw  
     // mat4.translate(mvMatrix, [0, 0, 0]);
    
      // Draw the star in its main color
      gl.bindTexture(gl.TEXTURE_2D, starTexture);
      gl.uniform1i(shaderProgram.samplerUniform, 0);
    
      gl.bindBuffer(gl.ARRAY_BUFFER, starVertexTextureCoordBuffer);
      gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, starVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);
    
      gl.bindBuffer(gl.ARRAY_BUFFER, starVertexPositionBuffer);
      gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, starVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
    
      setMatrixUniforms();
      gl.drawArrays(gl.TRIANGLES, 0, starVertexPositionBuffer.numItems);
    
      mvPopMatrix();
    

    }

    var lastTime = 0;

    function animate() {
    var timeNow = new Date().getTime();
    if (lastTime != 0) {
    var elapsed = timeNow - lastTime;

          // apply friction to speeds
          if( runSpeed &lt; 0){ 
              runSpeed += friction;
              if(runSpeed &gt; 0) {runSpeed=0;}else{}
          }else{
              runSpeed -= friction;
              if(runSpeed &lt; 0) {runSpeed=0;}else{}
          }
          
          // apply friction to speeds
          if( strafeSpeed &lt; 0){ 
              strafeSpeed += friction;
              if(strafeSpeed &gt; 0) {strafeSpeed=0;}else{}
          }else{
              strafeSpeed -= friction;
              if(strafeSpeed &lt; 0) {strafeSpeed=0;}else{}
          }
          
          pX += Math.cos(degToRad(90+oMouse.angleX)) * runSpeed * elapsed;                                     
          pZ +=  Math.sin(degToRad(90+oMouse.angleX)) * runSpeed * elapsed; 
          
          pX += Math.cos(degToRad(oMouse.angleX)) * strafeSpeed * elapsed;                                     
          pZ +=  Math.sin(degToRad(oMouse.angleX)) * strafeSpeed * elapsed; 
          
          pY += Math.sin(degToRad(oMouse.angleY)) * runSpeed * elapsed;
          
          
        
      }
      lastTime = timeNow;
    

    }

    function tick() {
    requestAnimFrame(tick);
    handleKeys();
    drawScene();
    animate();
    }

    function webGLStart() {
    var canvas = document.getElementById(“lesson09-canvas”);
    initGL(canvas);
    initShaders();
    initWorldObjects();
    initBuffers();
    initTexture();

      gl.clearColor(0.0, 0.0, 0.0, 1.0);
    
      document.onkeydown = handleKeyDown;
      document.onkeyup = handleKeyUp;
    
      tick();
    

    }

    var oMouse = {x:0,y:0,angleY:0.0,angleX:0.0}
    function mouseMoveHandler(e)
    {
    oMouse.x = e.pageX;
    oMouse.y = e.pageY;

    // calculate angle as percentage of window height/width
    var iH = (window).height(); var iW = (window).width();
    var x = oMouse.x;
    var y = oMouse.y;

    var oldAX = oMouse.angleX;
    var oldAY = oMouse.angleY;

    oMouse.angleX = (x/iW) * LOOK_ANGLE_RANGE;
    oMouse.angleY = (y/iH) * LOOK_ANGLE_RANGE;
    oMouse.angleY -= LOOK_ANGLE_RANGE/2;
    oMouse.angleX -= LOOK_ANGLE_RANGE/2;

    }

    $(document).ready(
    function()
    {

    fit(); $(window).resize( fit ); // fit canvas to window handler
    $(document).mousemove( mouseMoveHandler ); // mouse move handler
    getConsoleInput().keyup( consoleInputHandler ); // console command handler
    

    }
    );

// SCENE GRAPH

// BASE OBJECT //////////////////////////////
var obj_base = {};
obj_base.aDefaultProperties = { 
   position: [0.0, 0.0, 0.0] // world position
};




// QUAD OBJECT /////////////////////////////
 function obj_quad(aProperties)
 {
  // get defaults from base object, overwrite with arguments
  for(var k in obj_base.aDefaultProperties){ this[k] = obj_base.aDefaultProperties[k]; }  
  for(var k in aProperties){ this[k] = aProperties[k]; }   
  
  this.aVertexPositions = 
    [-1.0, -1.0, 0.0,
      1.0, -1.0, 0.0,
     -1.0,  1.0, 0.0,
     
     -1.0,  1.0, 0.0,
      1.0, -1.0, 0.0,
      1.0,  1.0, 0.0
     ]; 
     
  this.iTriangleCount = this.aVertexPositions.length / 9;

  this.aTextureCoords = 
     [0.0, 0.0,
      1.0, 0.0,
      0.0, 1.0,
      
      0.0, 1.0,
      1.0, 0.0,
      1.0, 1.0
    ]; 
 }
 
 obj_quad.prototype.getVertexPositions = function() // applies world position to local vertex positions
 {
  var aReturnVertexArray = [];
  var a = this.aVertexPositions;
  var p = this.position;
  for(var i = 0; i &lt; this.iTriangleCount;  i++){
    aReturnVertexArray = aReturnVertexArray.concat( 
      [ a[(i*9)]   + p[0], a[(i*9)+1] + p[1], a[(i*9)+2] + p[2] ] // vertex 1
     ,[ a[(i*9)+3] + p[0], a[(i*9)+4] + p[1], a[(i*9)+5] + p[2] ] // vertex 2
     ,[ a[(i*9)+6] + p[0], a[(i*9)+7] + p[1], a[(i*9)+8] + p[2] ] // vertex 3
                             
    );
  }
  return aReturnVertexArray;
 };
 
 obj_quad.prototype.getTextureCoords = function(){ return this.aTextureCoords; };
  
  
  
  
// SCENE GRAPH /////////////////////////////

// NODE oooo
function sceneNode(geometry){
  this.iParentIndex = -1;
  this.aChildrenIndices= [];
  this.geometry = geometry;
}

sceneNode.prototype.setParent = function(index){ 
  var oldParent = this.iParentIndex; 
  this.iParentIndex = index;
  return oldParent;
};

sceneNode.prototype.hasChildren = function(){ return this.aChildrenIndices.length &gt; 0; };



// GRAPH oooo
function sceneGraph(){
  // vertex data
  this.aVertexPositions = [];
  this.aTextureCoords = [];
  
  // graph
  this.graph = [];
  
  // create root node  
  this.rootIndex = this.addChild(new sceneNode(null), null);
}

sceneGraph.prototype.addChild = function(node, parentIndex)
{
  var iNodeIndex = this.graph.push(node) - 1;
  if(parentIndex != null){
    node.setParent(parentIndex);
    this.getNode(parentIndex).aChildrenIndices.push(iNodeIndex);
  }else{}
  return iNodeIndex;
};

sceneGraph.prototype.getRoot = function(){ return this.getNode(this.rootIndex); };
sceneGraph.prototype.getNode = function(index){ return this.graph[index]; };

sceneGraph.prototype.clearBuffers = function(){ this.aVertexPositions = []; this.aTextureCoords = []; };

sceneGraph.prototype.rebuildBuffers = function(){
  this.clearBuffers();
  this.rebuildBuffersAtNode( this.getRoot() );
};
sceneGraph.prototype.rebuildBuffersAtNode = function(node) 
{      
  
  // PROCESS SELF
  //  concat geometry and texture coords to list
  
  if(node.geometry != null){
 //  this.aVertexPositions = this.aVertexPositions.concat(node.geometry.getVertexPositions());
 //  this.aTextureCoords = this.aTextureCoords.concat(node.geometry.getTextureCoords());
   
   var aPos = node.geometry.getVertexPositions();
   var aPosL = aPos.length;
   var aTex = node.geometry.getTextureCoords();
   var aTexL = aTex.length;
   for(var i = 0; i&lt; aPosL; i++){
    this.aVertexPositions.push(aPos[i]);
   }
   for(var i = 0; i&lt; aTexL; i++){
    this.aTextureCoords.push(aTex[i]);
   }
  }else{}
  
  // PROCESS CHILDREN
  //  no children? do nothing, else rebuild foreach child 
  if(!node.hasChildren()){ return; }
  else{
    var children = node.aChildrenIndices;
    for(var i = 0; i&lt;children.length; i++){
      this.rebuildBuffersAtNode(this.getNode( children[i] ));
    }      
  }
};

</script>

</head>

<body onload=“webGLStart();”>

&lt;canvas id="lesson09-canvas" style="border: none;" width="569" height="320"&gt;&lt;/canvas&gt;





&lt;div class="console"&gt;&lt;span&gt;&lt;/span&gt;&lt;input type="text" /&gt;&lt;/div&gt;

</body>

</html>

OK that first post is so long Here’s a picture to pay you guys back of what my neo-natal engine is doing. I can’t get this to drop below 60fps so I love a single call to drawArrays per frame

The question is simpified as this

  1. my sceneGraph builds geometry array once then just drawArrays it over and over

  2. nodes are quads look like this:


 function obj_quad(aProperties)
     {
      // get defaults from base object, overwrite with arguments
      for(var k in obj_base.aDefaultProperties){ this[k] = obj_base.aDefaultProperties[k]; }  
      for(var k in aProperties){ this[k] = aProperties[k]; }   
      
      this.aVertexPositions = 
        [-1.0, -1.0, 0.0,
          1.0, -1.0, 0.0,
         -1.0,  1.0, 0.0,
         
         -1.0,  1.0, 0.0,
          1.0, -1.0, 0.0,
          1.0,  1.0, 0.0
         ]; 
         
      this.iTriangleCount = this.aVertexPositions.length / 9;
   
      this.aTextureCoords = 
         [0.0, 0.0,
          1.0, 0.0,
          0.0, 1.0,
          
          0.0, 1.0,
          1.0, 0.0,
          1.0, 1.0
        ]; 
     }
     
     obj_quad.prototype.getVertexPositions = function() // applies world position to local vertex positions
     {
      var aReturnVertexArray = [];
      var a = this.aVertexPositions;
      var p = this.position;
      for(var i = 0; i < this.iTriangleCount;  i++){
        aReturnVertexArray = aReturnVertexArray.concat( 
          [ a[(i*9)]   + p[0], a[(i*9)+1] + p[1], a[(i*9)+2] + p[2] ] // vertex 1
         ,[ a[(i*9)+3] + p[0], a[(i*9)+4] + p[1], a[(i*9)+5] + p[2] ] // vertex 2
         ,[ a[(i*9)+6] + p[0], a[(i*9)+7] + p[1], a[(i*9)+8] + p[2] ] // vertex 3
                                 
        );
      }
      return aReturnVertexArray;
     };
     
     obj_quad.prototype.getTextureCoords = function(){ return this.aTextureCoords; };

  1. QUESTION: what is the best way to do per object transforms when you have a single call to drawarrays?
    a) in javascript per object via transform matrices in obj_quad’s

b) in shader by some kind of massive array of transform matrices (one for each object) that paralles the single vertex array I have?

c) none of the above - I’m crazy.

COMPLETE CODE AGAIN PASTED CORRECTLY (also see github above)



<!DOCTYPE html> 
<html> 
 
<head> 
<title>Soft Voxels</title> 
<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> 
<style> 
html,body{ overflow:hidden;background:black; cursor:text;}
 
canvas {
  position: absolute;
  top: 0;
  left: 0;
  background:black;
}
 
.console{
    position:absolute;
    z-index:1000;
    top:-400px;
    left:0;
    background:transparent;
    height:400px;
    width:33%;
    opacity:0.90;
}
.console span{
    display:block;
    position:absolute;
    top:0px;
    bottom:32px;
    left:0;
    right:0;
    width:auto;
    height:auto;
    background:#343942;
    font-family:'Dina', 'Courier New', 'Courier', monospace;
    font-size:15px;    
    color:white;
    overflow-y:scroll;
    overflow-x:hidden;
}
.console input{
    position:absolute;
    bottom:3px;
    width:auto;
    left:0;
    right:0;
    height:20px;
}
</style> 
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script> 
<script type="text/javascript" src="glMatrix-0.9.5.min.js"></script> 
<script type="text/javascript" src="webgl-utils.js"></script> 
/* star shader
<script id="shader-fs" type="x-shader/x-fragment"> 
    #ifdef GL_ES
    precision highp float;
    #endif
 
    varying vec2 vTextureCoord;
 
    uniform sampler2D uSampler;
 
 
    void main(void) {
        vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
     if ( textureColor.a < 0.4 ) discard ;
        gl_FragColor = textureColor;// * vec4(uColor, 1.0);
    }
</script> 
 
<script id="shader-vs" type="x-shader/x-vertex"> 
    attribute vec3 aVertexPosition;
    attribute vec2 aTextureCoord;
 
    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;
 
    varying vec2 vTextureCoord;
 
    void main(void) {
        gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
        vTextureCoord = aTextureCoord;
    }
</script> 
 
 
<script type="text/javascript"> 
 
    var bL = true; // logging on or off?
    var aCommandLog = []; // stack of commands entered
    var iCommandLogIndex = -1; // position in command stack
    // console comamnd names
    var CC_CLEAR = 'clear';
    var CC_HELP  = '?';
    
    var LOOK_ANGLE_RANGE = 540;
    var VIEW_VOLUME_SIZE = 1000;
    var KEY_W = 87;
    var KEY_A = 65;
    var KEY_S = 83;
    var KEY_D = 68;
    
    var KEY_TILDE        = 192;
    var KEY_ENTER        = 13;
    var KEY_UP           = 38;
    var KEY_DOWN         = 40;
    
    
    
    
    
    
    
    
    
    
    var gl;
    var fFOV = 45.0 // field of view
    
    
    var consoleLineNumber = 0;
    
 
    var zoom = -15;
 
    var tilt = 10;
    var spin = 0;
    var pan = 0;
 
    var runSpeed = 0;
    var runStep = 0.001;
    
    var strafeSpeed = 0;
    var pX = 0;
    var pY =0;
    var pZ = 0;
    
    var friction = .0007;
    
    
    
 
    
    // fit canvas to window
    function fit(){ $('canvas').css({width:$(window).width()+'px'});}
 
    
    function handleKeys() 
    {
        
        // RUN FORWARD
        if (currentlyPressedKeys[KEY_W]) { //w
            // Page Down
            zoom += 0.1;
            runSpeed += runStep;
            
        }
        // STRAFE LEFT 
        if (currentlyPressedKeys[KEY_A]) { //a
            // Up cursor key
            pan += 0.1;
            strafeSpeed +=runStep;
        }
        // RUN BACKWARDS
        if (currentlyPressedKeys[KEY_S]) { //s
            // Page Up
            zoom -= 0.1;
            runSpeed -=runStep;
            
        }
        // STRAFE RIGHT 
        if (currentlyPressedKeys[KEY_D]) { //d
            // Down cursor key
            pan -= 0.1;
            strafeSpeed -= runStep;
        }
        // TOGGLE CONSOLE
        if (currentlyPressedKeys[KEY_TILDE] ) { // ~ `
            // Down cursor key
           toggleConsole();
        }
        
        
        
    }
    
    
    
   
    // browser console    
    function lC(s){ if(bL){console.log(s);}else{}}    
 
    // engine  console
    function l(s)
    {     
     if(bL){ 
      consoleLineNumber++; var oConsole =   getConsoleLog();       
      oConsole.html( 
       oConsole.html() 
       + consoleLineNumber 
       + ''
       + s 
       + '
'
      );  
      oConsole[0].scrollTop = oConsole[0].scrollHeight;      
     }else{}      
    }  
    function getConsoleLog(){ return getConsole().find('span'); }
    function getConsole(){ return $('.console');}
    function getConsoleInput(){ return $('.console input');}
    function setConsoleInput(sCmd){ getConsoleInput().val(sCmd); }
    function clearConsoleInput(){ getConsoleInput().val('');}
    function clearConsole(){ getConsoleLog().html(''); }
    var bInConsoleToggle = false;
    function toggleConsole()
    {
        if(bInConsoleToggle){return false;}else{}
        bInConsoleToggle = true;
		
        
        var o = getConsole();
        
        var iTop = 0;
        if(o.offset().top &gt; -1){
            iTop = -o.height();
            getConsoleInput().blur();
        }else{ getConsoleInput().focus(); }
        o.animate(
        {
            top: iTop + 'px'
        }
        ,150
        ,'swing'
        ,function(){ clearConsoleInput(); bInConsoleToggle = false;  }
        );        
    }
    function consoleSaveCommand(sCmd)
    {
      aCommandLog.push(sCmd);
      iCommandLogIndex = aCommandLog.length -1;
    }
    
    // run console command and log command and output
    function consoleCommand(sCmd) 
    {   
     consoleSaveCommand(sCmd);
     clearConsoleInput();
     
     var sOutput = sCmd; 
     switch(sCmd){
     
      case CC_CLEAR: CC_clear(); return /* special case - don't log clear */; break;
      
      case CC_HELP: sOutput += CC_help(); break;
      
      default: sOutput += CC_bad();
     }
     
     l(sOutput);
    }
    
    function consoleInputHandler(e)
    {
      var iKeyCode = e.keyCode;
      if(iKeyCode == KEY_ENTER){ 
        getConsole();
        consoleCommand(e.srcElement.value);
      }else{}
      
      if(iKeyCode == KEY_UP){ // backup command
        setConsoleInput(aCommandLog[iCommandLogIndex]);
        iCommandLogIndex--; if(iCommsdddddddddandLogIndex &lt; 0 ){iCommandLogIndex = 0; }else{}
        
      }
      if(iKeyCode == KEY_DOWN){ // next command
        setConsoleInput(aCommandLog[iCommandLogIndex]);
        iCommandLogIndex++; if(iCommandLogIndex &gt;aCommandLog.length-1){iCommandLogIndex = aCommandLog.length-1; }else{}
      }
    }
    // actual commands for engine console
    function CC_bad(){ return '
command doesn\'t exist or command malformed'; }
    function CC_clear(){ clearConsole(); return ''; }
    function CC_help(){ return '
Commands
* clear - clear console
* ? - this help message'; }
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    function initGL(canvas) {
      try {
        gl = canvas.getContext("experimental-webgl");
        gl.viewportWidth = canvas.width;
        gl.viewportHeight = canvas.height;
        
       // perspective
        mat4.perspective(fFOV, gl.viewportWidth / gl.viewportHeight, 0.1, VIEW_VOLUME_SIZE, pMatrix);
          
       // when we want transparency we can do the following:
       //   gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
       //   gl.enable(gl.BLEND);
    
       // we moved this so we don't do it every frame 
          gl.enable (gl.DEPTH_TEST);
      } catch (e) {}
      
      if (!gl) {
          alert("Could not initialise WebGL: try get.webgl.org");
      }
    }
 
 
    function getShader(gl, id) {
        var shaderScript = document.getElementById(id);
        if (!shaderScript) {
            return null;
        }
 
        var str = "";
        var k = shaderScript.firstChild;
        while (k) {
            if (k.nodeType == 3) {
                str += k.textContent;
            }
            k = k.nextSibling;
        }
 
        var shader;
        if (shaderScript.type == "x-shader/x-fragment") {
            shader = gl.createShader(gl.FRAGMENT_SHADER);
        } else if (shaderScript.type == "x-shader/x-vertex") {
            shader = gl.createShader(gl.VERTEX_SHADER);
        } else {
            return null;
        }
 
        gl.shaderSource(shader, str);
        gl.compileShader(shader);
 
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            alert(gl.getShaderInfoLog(shader));
            return null;
        }
 
        return shader;
    }
 
 
    var shaderProgram;
 
    function initShaders() {
        var fragmentShader = getShader(gl, "shader-fs");
        var vertexShader = getShader(gl, "shader-vs");
 
        shaderProgram = gl.createProgram();
        gl.attachShader(shaderProgram, vertexShader);
        gl.attachShader(shaderProgram, fragmentShader);
        gl.linkProgram(shaderProgram);
 
        if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
            alert("Could not initialise shaders");
        }
 
        gl.useProgram(shaderProgram);
 
        shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); // gets an integer, on inspection this is 0
        gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
 
        shaderProgram.textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord"); // gets an integer, on inspection this is 1
        gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute);
 
        shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix"); // projection matrix -&gt; clipping and field of view, perspective
        shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix"); // modelview matrix -&gt; puts camera in scene
        shaderProgram.samplerUniform = gl.getUniformLocation(shaderProgram, "uSampler");
     
    }
 
 
    function handleLoadedTexture(texture) {
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.image);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
        gl.generateMipmap(gl.TEXTURE_2D);
        
        gl.bindTexture(gl.TEXTURE_2D, null);
    }
 
 
    var starTexture;
 
    function initTexture() {
        starTexture = gl.createTexture();
        starTexture.image = new Image();
        starTexture.image.onload = function () {
            handleLoadedTexture(starTexture)
        }
 
        starTexture.image.src = "grass-billboard.png";
    }
 
 
    var mvMatrix = mat4.create();
    var mvMatrixStack = [];
    var pMatrix = mat4.create();
 
    function mvPushMatrix() {
        var copy = mat4.create();
        mat4.set(mvMatrix, copy);
        mvMatrixStack.push(copy);
    }
 
    function mvPopMatrix() {
        if (mvMatrixStack.length == 0) {
            throw "Invalid popMatrix!";
        }
        mvMatrix = mvMatrixStack.pop();
    }
 
 
    function setMatrixUniforms() {
        gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix);
        gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix);
    }
 
 
    function degToRad(degrees) {
        return degrees * Math.PI / 180;
    }
 
 
    var currentlyPressedKeys = {};
 
    function handleKeyDown(event) {
        currentlyPressedKeys[event.keyCode] = true;
    }
 
 
    function handleKeyUp(event) {
        currentlyPressedKeys[event.keyCode] = false;
    }
 
 
    var starVertexPositionBuffer;
    var starVertexTextureCoordBuffer;
 
    function initBuffers() {
        starVertexPositionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, starVertexPositionBuffer); // gives us a pointer into a store
       
        vertices = scene.aVertexPositions;
        
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); // actually allocates stoe and pushes data up
        starVertexPositionBuffer.itemSize = 3;
        starVertexPositionBuffer.numItems = vertices.length / starVertexPositionBuffer.itemSize;
 
        starVertexTextureCoordBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, starVertexTextureCoordBuffer);
        
        var textureCoords = scene.aTextureCoords;
        
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW);
        starVertexTextureCoordBuffer.itemSize = 2;
        starVertexTextureCoordBuffer.numItems = textureCoords.length/starVertexTextureCoordBuffer.itemSize;
        
        return;
    }
 
 
    function drawStar() {
     //   gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, starTexture);
        gl.uniform1i(shaderProgram.samplerUniform, 0);
 
        gl.bindBuffer(gl.ARRAY_BUFFER, starVertexTextureCoordBuffer);
        gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, starVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);
 
        gl.bindBuffer(gl.ARRAY_BUFFER, starVertexPositionBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, starVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
 
        setMatrixUniforms();
        gl.drawArrays(gl.TRIANGLES, 0, starVertexPositionBuffer.numItems);
    }
 
 
 
    function Star(startingDistance, rotationSpeed, position) {
        this['x'] = position[0];
        this['y'] = position[1];
        this['z'] = position[2];
        
        this.angle = 0;
        this.dist = startingDistance;
        this.rotationSpeed = rotationSpeed;
 
    }
 
    Star.prototype.draw = function () {
        mvPushMatrix();
 
        // Move to the star's positionw  
        mat4.translate(mvMatrix, [this.x, this.y, this.z]);
 
        // Draw the star in its main color
        drawStar()
 
        mvPopMatrix();
    };
 
 
    var effectiveFPMS = 60 / 1000;
    Star.prototype.animate = function (elapsedTime) {
  
 
    };
 
 
 
    var stars = [];
 
    // random half negative
    function rndHN(iRange){
        return (Math.random() * iRange) - iRange/2;
    }
    
    var scene = null;
    // star constructor:   function Star(startingDistance, rotationSpeed) {
    function initWorldObjects() {
        scene = new sceneGraph();
        var numBushes = 78530;
        for (var i=0; i &lt; numBushes; i++) {
          scene.addChild(
            new sceneNode(
              new obj_quad({ position:
                              [rndHN(333)
                              ,rndHN(1.1)
                              ,rndHN(303)]
                            })
            )
            , 0);
        
        }
       var i = new Date().getTime();
        scene.rebuildBuffers(); // generate nice arrays of vertices
        // array concat is slow  that's the problem traversing only takes like 4 ms
          console.log(new Date().getTime() - i);
    }
 
 
    function drawScene() {
        // is this needed? I commented it out and behavior is the same ??
        //gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
       
        gl.clear(gl.COLOR_BUFFER_BIT );
 
        // THIS IS DONE IN INIT NOW since it need only be done once
        //    mat4.perspective(fFOV, gl.viewportWidth / gl.viewportHeight, 0.1, VIEW_VOLUME_SIZE, pMatrix);
        
      
     
        mat4.identity(mvMatrix);
        
        mat4.rotate(mvMatrix, degToRad(oMouse.angleY), [1.0, 0.0, 0.0]);
        mat4.rotate(mvMatrix, degToRad(oMouse.angleX), [0.0, 1.0, 0.0]);
        mat4.translate(mvMatrix, [pX, pY, pZ]);
 
        mvPushMatrix();
 
        // Move to the star's positionw  
       // mat4.translate(mvMatrix, [0, 0, 0]);
 
        // Draw the star in its main color
        gl.bindTexture(gl.TEXTURE_2D, starTexture);
        gl.uniform1i(shaderProgram.samplerUniform, 0);
 
        gl.bindBuffer(gl.ARRAY_BUFFER, starVertexTextureCoordBuffer);
        gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, starVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);
 
        gl.bindBuffer(gl.ARRAY_BUFFER, starVertexPositionBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, starVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
 
        setMatrixUniforms();
        gl.drawArrays(gl.TRIANGLES, 0, starVertexPositionBuffer.numItems);
 
        mvPopMatrix();
 
    }
 
 
    var lastTime = 0;
 
    function animate() {
        var timeNow = new Date().getTime();
        if (lastTime != 0) {
            var elapsed = timeNow - lastTime;
            
            // apply friction to speeds
            if( runSpeed &lt; 0){ 
                runSpeed += friction;
                if(runSpeed &gt; 0) {runSpeed=0;}else{}
            }else{
                runSpeed -= friction;
                if(runSpeed &lt; 0) {runSpeed=0;}else{}
            }
            
            // apply friction to speeds
            if( strafeSpeed &lt; 0){ 
                strafeSpeed += friction;
                if(strafeSpeed &gt; 0) {strafeSpeed=0;}else{}
            }else{
                strafeSpeed -= friction;
                if(strafeSpeed &lt; 0) {strafeSpeed=0;}else{}
            }
            
            pX += Math.cos(degToRad(90+oMouse.angleX)) * runSpeed * elapsed;                                     
            pZ +=  Math.sin(degToRad(90+oMouse.angleX)) * runSpeed * elapsed; 
            
            pX += Math.cos(degToRad(oMouse.angleX)) * strafeSpeed * elapsed;                                     
            pZ +=  Math.sin(degToRad(oMouse.angleX)) * strafeSpeed * elapsed; 
            
            pY += Math.sin(degToRad(oMouse.angleY)) * runSpeed * elapsed;
            
            
          
        }
        lastTime = timeNow;
 
    }
 
 
    function tick() {
        requestAnimFrame(tick);
        handleKeys();
        drawScene();
        animate();
    }
 
 
 
    function webGLStart() {
        var canvas = document.getElementById("lesson09-canvas");
        initGL(canvas);
        initShaders();
        initWorldObjects();
        initBuffers();
        initTexture();
        
 
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
 
        document.onkeydown = handleKeyDown;
        document.onkeyup = handleKeyUp;
 
        tick();
    }
 
    
    var oMouse = {x:0,y:0,angleY:0.0,angleX:0.0}
    function mouseMoveHandler(e)
    {
     oMouse.x = e.pageX;
     oMouse.y = e.pageY;
     
     // calculate angle as percentage of window height/width
     var iH = $(window).height();
     var iW = $(window).width();
     var x = oMouse.x;
     var y = oMouse.y;
     
     
     var oldAX = oMouse.angleX;
     var oldAY = oMouse.angleY;
     
     oMouse.angleX = (x/iW) * LOOK_ANGLE_RANGE;
     oMouse.angleY = (y/iH) * LOOK_ANGLE_RANGE;
     oMouse.angleY -= LOOK_ANGLE_RANGE/2;
     oMouse.angleX -= LOOK_ANGLE_RANGE/2;
     
    }
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    $(document).ready(
    function()
    {
    
      fit(); $(window).resize( fit ); // fit canvas to window handler
      $(document).mousemove( mouseMoveHandler ); // mouse move handler
      getConsoleInput().keyup( consoleInputHandler ); // console command handler
    
    }
    );
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
// SCENE GRAPH    
    
    
    // BASE OBJECT //////////////////////////////
    var obj_base = {};
    obj_base.aDefaultProperties = { 
       position: [0.0, 0.0, 0.0] // world position
    };
    
    
    
    
    // QUAD OBJECT /////////////////////////////
     function obj_quad(aProperties)
     {
      // get defaults from base object, overwrite with arguments
      for(var k in obj_base.aDefaultProperties){ this[k] = obj_base.aDefaultProperties[k]; }  
      for(var k in aProperties){ this[k] = aProperties[k]; }   
      
      this.aVertexPositions = 
        [-1.0, -1.0, 0.0,
          1.0, -1.0, 0.0,
         -1.0,  1.0, 0.0,
         
         -1.0,  1.0, 0.0,
          1.0, -1.0, 0.0,
          1.0,  1.0, 0.0
         ]; 
         
      this.iTriangleCount = this.aVertexPositions.length / 9;
   
      this.aTextureCoords = 
         [0.0, 0.0,
          1.0, 0.0,
          0.0, 1.0,
          
          0.0, 1.0,
          1.0, 0.0,
          1.0, 1.0
        ]; 
     }
     
     obj_quad.prototype.getVertexPositions = function() // applies world position to local vertex positions
     {
      var aReturnVertexArray = [];
      var a = this.aVertexPositions;
      var p = this.position;
      for(var i = 0; i &lt; this.iTriangleCount;  i++){
        aReturnVertexArray = aReturnVertexArray.concat( 
          [ a[(i*9)]   + p[0], a[(i*9)+1] + p[1], a[(i*9)+2] + p[2] ] // vertex 1
         ,[ a[(i*9)+3] + p[0], a[(i*9)+4] + p[1], a[(i*9)+5] + p[2] ] // vertex 2
         ,[ a[(i*9)+6] + p[0], a[(i*9)+7] + p[1], a[(i*9)+8] + p[2] ] // vertex 3
                                 
        );
      }
      return aReturnVertexArray;
     };
     
     obj_quad.prototype.getTextureCoords = function(){ return this.aTextureCoords; };
      
      
      
      
    // SCENE GRAPH /////////////////////////////
    
    // NODE oooo
    function sceneNode(geometry){
      this.iParentIndex = -1;
      this.aChildrenIndices= [];
      this.geometry = geometry;
    }
    
    sceneNode.prototype.setParent = function(index){ 
      var oldParent = this.iParentIndex; 
      this.iParentIndex = index;
      return oldParent;
    };
    
    sceneNode.prototype.hasChildren = function(){ return this.aChildrenIndices.length &gt; 0; };
    
    
    
    // GRAPH oooo
    function sceneGraph(){
      // vertex data
      this.aVertexPositions = [];
      this.aTextureCoords = [];
      
      // graph
      this.graph = [];
      
      // create root node  
      this.rootIndex = this.addChild(new sceneNode(null), null);
    }
    
    sceneGraph.prototype.addChild = function(node, parentIndex)
    {
      var iNodeIndex = this.graph.push(node) - 1;
      if(parentIndex != null){
        node.setParent(parentIndex);
        this.getNode(parentIndex).aChildrenIndices.push(iNodeIndex);
      }else{}
      return iNodeIndex;
    };
    
    sceneGraph.prototype.getRoot = function(){ return this.getNode(this.rootIndex); };
    sceneGraph.prototype.getNode = function(index){ return this.graph[index]; };
    
    sceneGraph.prototype.clearBuffers = function(){ this.aVertexPositions = []; this.aTextureCoords = []; };
    
    sceneGraph.prototype.rebuildBuffers = function(){
      this.clearBuffers();
      this.rebuildBuffersAtNode( this.getRoot() );
    };
    sceneGraph.prototype.rebuildBuffersAtNode = function(node) 
    {      
      
      // PROCESS SELF
      //  concat geometry and texture coords to list
      
      if(node.geometry != null){
     //  this.aVertexPositions = this.aVertexPositions.concat(node.geometry.getVertexPositions());
     //  this.aTextureCoords = this.aTextureCoords.concat(node.geometry.getTextureCoords());
       
       var aPos = node.geometry.getVertexPositions();
       var aPosL = aPos.length;
       var aTex = node.geometry.getTextureCoords();
       var aTexL = aTex.length;
       for(var i = 0; i&lt; aPosL; i++){
        this.aVertexPositions.push(aPos[i]);
       }
       for(var i = 0; i&lt; aTexL; i++){
        this.aTextureCoords.push(aTex[i]);
       }
      }else{}
      
      // PROCESS CHILDREN
      //  no children? do nothing, else rebuild foreach child 
      if(!node.hasChildren()){ return; }
      else{
        var children = node.aChildrenIndices;
        for(var i = 0; i&lt;children.length; i++){
          this.rebuildBuffersAtNode(this.getNode( children[i] ));
        }      
      }
    };
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
&lt;/script&gt; 
 
 
&lt;/head&gt; 
 
 
&lt;body onload="webGLStart();"&gt; 
   
    &lt;canvas id="lesson09-canvas" style="border: none;" width="569" height="320"&gt;&lt;/canvas&gt; 
 
    
    
    
    
    &lt;div class="console"&gt;&lt;span&gt;&lt;/span&gt;&lt;input type="text" /&gt;&lt;/div&gt; 
    
    
    
    
    
    
    
    
    
&lt;/body&gt; 
 
&lt;/html&gt;

OK so my solution to using a single call to drawArrays with lots of animated objects is to use a glMatrix transform matrix against the animated objects vertices and bufferSubData() the objects new vertex positions into the single vertexbuffer

[ol]
[li]When I traverse the scene graph before the render loop, store the objects index into the single vertex buffer for use as the offset in bufferSubData(), also add the item to an ‘animatable’ list[/:m:3gks6bm8][/li][li]on each frame, for each item in the ‘animatable’ list, apply the objects transforms to its vertices in js [/:m:3gks6bm8][/li][li]bufferSubData() to the gpu the result[/:m:3gks6bm8][/li][li]Profit[/:m:3gks6bm8][/ol][/li]
This will work I’m sure of it. I did a test and managed to get some nice sine wave action. The texture coords seem to be screwed which makes no sense but I’ll figure that out later.

[/quote]

I got this technique I propose above of doing bufferSubData() working in super simplified fashion :D. I found I could do several THOUSAND buffersubdata’s before fps had any hit which means this is the right way to go.

For now from github you’ll see I’m doing the calculation in JS via glMatrix.

My thought is to have a three new VBOs for position, rotation, and scale and instead of doing the transform math in JS to just buffersubdata() the position,rotation,scale values from JS and do the actual transform math in the vertex shader.

This should be amazingly fast and give me around 10,000+ animated objects per frame in 1,000,000+ polygons with framerate locked at 60fps.

Once my engine can do this reliably I’ll make a basic game to demonstrate it works. My only game I did last year an HTML5 2d canvas tetris clone if anyone is curious It’s awesome. Download it : GitHub - johnnyscott/Johnny-Scott-s-HTML5-Tetrominoes: Create a javascript / canvas driven Tetris clone

Here’s a screen shot:

Also I’m a fantastic artist and a great programmer so by the time I figure this out I ought to be a great indie game dev LOL Here’s a sketch of my dog I did:

See my progress, server is up from time to time