Related
I am fairly new to WebGL and I am working on a 3D game that dynamically generates land around the player. So, I am trying to add vertices to draw in game. Things worked fine, until I started to add this feature, making hundreds of gl.drawArrays() calls per frame, which made it super laggy. After having done some research I found that a better way to approach this is to make a huge array containing all of the vertices (each shape separated by degenerate triangles) and then make one gl.drawArray() call per frame.
Here is the part of my code that runs on load:
function loadGraphics() {
// ground
// buffers
quadVertexPositionBuffer = gl.createBuffer();
vertices = [];
verticesItemCount = 0;
quadVertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quadVertexColorBuffer);
var colors = [
0.0, 0.4, 0.0, 1.0,
0.0, 0.4, 0.0, 1.0,
0.0, 0.4, 0.0, 1.0,
0.0, 0.4, 0.0, 1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.DYNAMIC_DRAW);
quadVertexColorBuffer.itemSize = 4;
quadVertexColorBuffer.numItems = 4;
}
Here is the part that runs per frame:
function drawGraphics() {
// draw code for graphics
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
gl.clearColor(0.35, 0.4, 1.0, 1.0 );
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);
mat4.identity(mvMatrix);
// perspective
var cameraX = camera.x, cameraY = camera.y, cameraZ = camera.z;
mat4.rotate(mvMatrix, rotMatrix[1], [1, 0, 0]);
mat4.rotate(mvMatrix, rotMatrix[0], [0, 1, 0]);
mat4.translate(mvMatrix, [-cameraX/33, -cameraY/33, -cameraZ/33]);
debug.add("{camera} x:"+camera.x+",y:"+camera.y+",z:"+camera.z+";");
debug.add("\n{mouse delta} x:"+mouse.x-mouse.prevX+",y:"+mouse.y-mouse.prevY+";");
debug.add("\n{rm}[0]:"+rotMatrix[0]+",[1]:"+rotMatrix[1]);
// ground
gl.bindBuffer(gl.ARRAY_BUFFER, quadVertexColorBuffer);
gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, quadVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);
// land plots
vertices = [];
verticesItemCount = 0;
for (var i = 0; i < landPlots.length; i++) {
var oX = landPlots[i].x*3;
var oZ = landPlots[i].z*3;
var plotVertices = [
-1.5+oX, 0.0, 1.5+oZ, 1.0,
1.5+oX, 0.0, 1.5+oZ, 1.0,
-1.5+oX, 0.0, -1.5+oZ, 1.0,
1.5+oX, 0.0, -1.5+oZ, 1.0
];
pushDrawArray(plotVertices, 4);
for(var j = 1; j <= 2; j++) {
debug.add(" " + renderLandPlotIntersection(landPlots[i], j));
}
}
gl.bindBuffer(gl.ARRAY_BUFFER, quadVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, 4, gl.FLOAT, false, 0, 0);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.DYNAMIC_DRAW);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, verticesItemCount);
}
function renderLandPlotIntersection(landPlot, side) {
var x = landPlot.x;
var z = landPlot.z;
var lvl = landPlot.level;
var olvl = null;
var plot;
switch (side) {
case 0: plot =getLandPlot(x-1, z ); if (plot !== null) olvl = plot.level/66; else return 0; break;
case 1: plot =getLandPlot(x, z+1); if (plot !== null) olvl = plot.level/66; else return 0; break;
case 2: plot =getLandPlot(x+1, z ); if (plot !== null) olvl = plot.level/66; else return 0; break;
case 3: plot =getLandPlot(x, z-1); if (plot !== null) olvl = plot.level/66; else return 0; break;
default: throw "Land plot intersection drawing: side out of range."; return -1;
}
var intersectionVertices = [
x*3, lvl, z*3,
x*3, lvl, z*3,
x*3, olvl, z*3,
x*3, olvl, z*3
];
pushDrawArray(intersectionVertices, 4);
return +1;
}
function pushDrawArray(array, itemCount) {
if (vertices.length > 0) {
// degenerate
vertices.push(vertices[vertices.length-3]);
vertices.push(vertices[vertices.length-2]);
vertices.push(vertices[vertices.length-1]);
vertices.push(array[0]);
vertices.push(array[1]);
vertices.push(array[2]);
verticesItemCount += 2 ;
}
gl.bufferSubData(gl.ARRAY_BUFFER, verticesItemCount*4, array);
verticesItemCount += itemCount;
}
I started using DYNAMIC_DRAW, though I don't really know how to use it. verticesItemCount marks how many vertices are in the vertices array.
gl.drawArrays() returns this error:
[.Offscreen-For-WebGL-060B7ED8]GL ERROR :GL_INVALID_OPERATION : glDrawArrays: attempt to access out of range vertices in attribute 1 localhost/:1 WebGL: too many errors, no more errors will be reported to the console for this context.
How can I fix this code to not cause an error?
I know what I did. I made the vertex position array longer than my vertex color array, so it was trying to access something out of bounds. The fix is to keep the vertex color array the same length as the vertex position array.
I'm trying to implement shadows using shadow maps, so I need to render a scene to a separate framebuffer (texture). I cannot get it to work properly, so after stripping down my codebase I'm left with a relatively simple set of instructions which should render a scene to a texture, and then simply render the texture.
The program consists of two programs:
Ground program
Teapot program
The first should render a rectangle, with a certain texture. The second one should render a teapot (with colors based on its position). Eech render step does the following (well, that's the idea anyway):
Switch to framebuffer
Render teapot
Switch to normal buffer
Render teapot
Render ground
Now, the ground fragment shader looks like:
gl_FragColor = texture2D(shadowMap, fTexCoord);
'shadowMap' is the texture I render to in step 2. I expect to see a floating teapot with a rectangle drawn under it. That indeed works. Now, I also expect to have the 'ground' to contain a teapot. After all, we rendered the scene we are looking at without the ground to the framebuffer/texture.
Code
var UNSIGNED_SHORT_SIZE = 2;
// Variables filled by setup()
var glCanvas;
var gl, teapotProgram, groundProgram;
var vBuffer, iBuffer, fBuffer;
var vertices, indices, textures;
var teapot = null;
var model;
var view;
var light;
var projection;
var BASE_URL = "https://hmbastiaan.nl/martijn/webgl/W08P02_SO/";
var WIDTH = 150, HEIGHT = 150;
function makeTeapot(){
var drawingInfo = teapot.getDrawingInfoObjects();
var indices = drawingInfo.indices;
for(var i=0; i < indices.length; i++){
indices[i] += 4; // Add offset for 'ground'
}
return {
indices: drawingInfo.indices,
vertices: drawingInfo.vertices
}
}
function makeRectangle(x1, x2, y1, y2, z1, z2){
var x1 = -2,
x2 = 2,
y1 = -1,
y2 = -1,
z1 = -1,
z2 = -5;
var vertices = [
vec4(x1, y2, z1, 1),
vec4(x2, y1, z1, 1),
vec4(x2, y1, z2, 1),
vec4(x1, y2, z2, 1)
];
var textures = [
vec2(-1.0, -1.0),
vec2( 1.0, -1.0),
vec2( 1.0, 1.0),
vec2(-1.0, 1.0)
];
var indices = [
0, 1, 2,
0, 2, 3
];
return {
indices: indices,
vertices: vertices,
textures: textures
}
}
function resetBuffers(){
vertices = [];
indices = [];
textures = [];
// Add rectangle
var rectangle = makeRectangle();
Array.prototype.push.apply(vertices, rectangle.vertices);
Array.prototype.push.apply(indices, rectangle.indices);
Array.prototype.push.apply(textures, rectangle.textures);
// Add teapot
var teapot = makeTeapot();
Array.prototype.push.apply(vertices, teapot.vertices);
Array.prototype.push.apply(indices, teapot.indices);
console.log(vertices);
console.log(indices);
console.log(textures);
// Send to GPU
gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
gl.bufferData(gl.ARRAY_BUFFER, flatten(vertices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, iBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
}
function setup(){
$.get(BASE_URL + "teapot.obj", function(teapot_obj_data){
teapot = new OBJDoc(BASE_URL + "teapot.obj");
if(!teapot.parse(teapot_obj_data, 1)){
alert("Parsing teapot.obj failed.");
return;
}
setup2();
}).fail(function(){
alert("Getting teapot.obj failed.");
});
}
function setup2(){
glCanvas = document.getElementById("gl-canvas");
gl = WebGLUtils.setupWebGL(glCanvas, {stencil: true, alpha: false});
gl.viewport(0, 0, WIDTH, HEIGHT);
teapotProgram = initShaders(gl, BASE_URL + "vshader-teapot.glsl", BASE_URL + "fshader-teapot.glsl");
groundProgram = initShaders(gl, BASE_URL + "vshader-ground.glsl", BASE_URL + "fshader-ground.glsl");
light = vec3(0.0, 2.0, -2.0);
view = lookAt(vec3(0, 0, 3), vec3(0,0,0), vec3(0,1,0));
projection = perspective(45, 1.0, 1, 100.0);
// Get teapot uniforms
gl.useProgram(teapotProgram);
teapotProgram.modelLoc = gl.getUniformLocation(teapotProgram, "Model");
teapotProgram.viewLoc = gl.getUniformLocation(teapotProgram, "View");
teapotProgram.projectionLoc = gl.getUniformLocation(teapotProgram, "Projection");
// Upload uniforms
gl.uniformMatrix4fv(teapotProgram.projectionLoc, false, flatten(projection));
gl.uniformMatrix4fv(teapotProgram.viewLoc, false, flatten(view));
gl.uniformMatrix4fv(teapotProgram.modelLoc, false, flatten(scalem(0.25, 0.25, 0.25)));
// Get teapot attributes
teapotProgram.vPosition = gl.getAttribLocation(teapotProgram, "vPosition");
// Get ground uniforms
gl.useProgram(groundProgram);
groundProgram.modelLoc = gl.getUniformLocation(groundProgram, "Model");
groundProgram.viewLoc = gl.getUniformLocation(groundProgram, "View");
groundProgram.projectionLoc = gl.getUniformLocation(groundProgram, "Projection");
groundProgram.shadowMap = gl.getUniformLocation(groundProgram, "shadowMap");
// Get ground attributes
groundProgram.vTexCoord = gl.getAttribLocation(groundProgram, "vTexCoord");
groundProgram.vPosition = gl.getAttribLocation(groundProgram, "vPosition");
// Allocate and fill vertices buffer
vBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
gl.vertexAttribPointer(teapotProgram.vPosition, 4, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(teapotProgram.vPosition);
gl.vertexAttribPointer(groundProgram.vPosition, 4, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(groundProgram.vPosition);
// Allocate indices buffer
iBuffer = gl.createBuffer();
// Setup FBO
fBuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fBuffer);
fBuffer.renderbuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, fBuffer.renderbuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, 512, 512);
fBuffer.texture = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, fBuffer.texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 512, 512, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, fBuffer.texture, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, fBuffer.renderbuffer);
// Sanity checking: framebuffer seems to throw now errors
if (!gl.isFramebuffer(fBuffer)) {
throw("Invalid framebuffer");
}
var status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
switch (status) {
case gl.FRAMEBUFFER_COMPLETE:
break;
case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
throw("Incomplete framebuffer: FRAMEBUFFER_INCOMPLETE_ATTACHMENT");
break;
case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
throw("Incomplete framebuffer: FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT");
break;
case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
throw("Incomplete framebuffer: FRAMEBUFFER_INCOMPLETE_DIMENSIONS");
break;
case gl.FRAMEBUFFER_UNSUPPORTED:
throw("Incomplete framebuffer: FRAMEBUFFER_UNSUPPORTED");
break;
default:
throw("Incomplete framebuffer: " + status);
}
// Set ground textures
gl.uniform1i(groundProgram.shadowMap, 0);
// Upload uniforms
gl.uniformMatrix4fv(groundProgram.projectionLoc, false, flatten(projection));
gl.uniformMatrix4fv(groundProgram.viewLoc, false, flatten(view));
gl.uniformMatrix4fv(groundProgram.modelLoc, false, flatten(mat4()));
// Restore default buffers
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// Set background colour
gl.clearColor(0.3921, 0.5843, 0.9294, 1.0);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
resetBuffers();
window.requestAnimationFrame(render);
}
function render(){
var teapot = makeTeapot();
gl.useProgram(teapotProgram);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
// Switch to framebuffer
gl.bindFramebuffer(gl.FRAMEBUFFER, fBuffer);
// Draw teapot
teapot = makeTeapot();
gl.drawElements(gl.TRIANGLES, teapot.indices.length, gl.UNSIGNED_SHORT, 6 * UNSIGNED_SHORT_SIZE);
// Set framebuffer to defualt buffer (in-browser output)
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// Draw ground
gl.useProgram(groundProgram);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
// Render teapot
gl.useProgram(teapotProgram);
gl.drawElements(gl.TRIANGLES, teapot.indices.length, gl.UNSIGNED_SHORT, 6 * UNSIGNED_SHORT_SIZE);
}
setup();
<div>
<br/>
<canvas width="150" height="150" id="gl-canvas">Sorry :|</canvas>
</div>
<script type='text/javascript' src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script type='text/javascript' src="https://hmbastiaan.nl/martijn/webgl/angel/webgl-utils.js"></script>
<script type='text/javascript' src="https://hmbastiaan.nl/martijn/webgl/angel/initShaders2.js"></script>
<script type='text/javascript' src="https://hmbastiaan.nl/martijn/webgl/angel/MV.js"></script>
<script type='text/javascript' src="https://hmbastiaan.nl/martijn/webgl/angel/objParser.js"></script>
Functions of interest:
setup2(): sets up all the buffers and uniforms.
render(): renders the scene.
Disclaimer: this is for an assignment, although this code is simplified enough to not look like the original assignment at all :).
At a glance there are several issues.
Texture bindings are global. Since in setup2 you unbind the 1 texture that means it's never used.
You need to bind whatever textures are needed before each draw call. In other words when you draw the ground you need to bind the teapot texture as in
gl.bindTexture(gl.TEXTURE_2D, fBuffer.texture);
Note: This is an over simplification of what's really needed. You really need to
Choose a texture unit to bind the texture to
var unit = 5;
gl.activeTexture(gl.TEXTURE0 + unit);
Bind the texture to that unit.
gl.bindTexture(gl.TEXTURE_2D, fBuffer.texture);
Set the uniform sampler to that texture unit
gl.uniform1i(groundProgram.shadowMap, unit);
The reason you don't need those extra steps is because (a) you only
have 1 texture so you're using texture unit #0, the default and (b) because
uniforms default to 0 so shadowMap is looking at texture unit #0.
Because you've made a mipmapped texture just rendering to level 0 will not update the mips.
In other words after you render the teapot you'll have a teapot in mip level 0 but mip levels 1, 2, 3, 4, 5 etc will still have nothing in them. You need to call
gl.generateMipmap(gl.TEXTURE_2D)
For that texture after you've rendered the teapot to it. Either that or stop using mips
You need to set the viewport every time you call gl.bindFramebuffer.
gl.bindFramebuffer should almost always be followed by a call to gl.viewport to make the viewport match the size of the thing you're rendering to
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
// set to size of fb
gl.viewport(0, 0, widthOfFb, heightOfFb);
renderSomething();
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// set to size of canvas's drawingBuffer
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
Attributes settings are global
You setup the teapot attributes. Then you draw a teapot to the texture. You then draw ground, but you're still using the teapot attributes.
Just like textures you need to setup attributes before each draw call.
I'm also guessing you really should not be calling makeTeapot in your render function but instead it should be called in setup.
You might find this article useful
You should also consider not putting properties on WebGL objects as it's arguably an anti-pattern.
Also synchronous XHR requests are not cool. You're getting this message in the JavaScript console
Synchronous XMLHttpRequest on the main thread is deprecated because
of its detrimental effects to the end user's experience. For more
help, check http://xhr.spec.whatwg.org/.
I am trying to draw 5161 cubes using webGL. The problem is not all cubes are drawn. Upon some searching, I think its because I am passing too many vertices in one VBO call. You can take a look at jsfiddle here: http://jsfiddle.net/n5fjhe21/. You can move around with QWERASDF and arrows keys but it isnt well implemented right now.
My draw call used to look like this:
function render(){
gl.uniformMatrix4fv(u_matrixLoc, false, new Float32Array(pMatrix));
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, data.triangles.length, gl.UNSIGNED_SHORT, 0);
}
So I would do is data.pushData() once and render as needed; It was fast. glObject is an array of Cubes.
data.pushData = function(){
// pushData once then call drawElements on every render call doesnt work as I hit some kind of limit;
// not all cubes are drawn; I think the draw calls must be split up;
data.vertices = [];
data.uv = [];
data.triangles = [];
var vertexOffset = 0;
glObjects.forEach(function pushingObject(o){
data.vertices.push.apply(data.vertices,o.vertices);
data.uv.push.apply(data.uv,o.uv);
o.triangles.forEach(function pushingTriangles(index){
data.triangles.push(index+vertexOffset);
});
vertexOffset += o.vertices.length/3; // change to component length later
});
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data.vertices),gl.DYNAMIC_DRAW );
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data.uv),gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, triangleBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(data.triangles), gl.DYNAMIC_DRAW );
};
But the problem (I think) is that I am passing in too many vertices at once. So I tried to merge pushData and render together:
data.render = function(){
data.vertices = [];
data.uv = [];
data.triangles = [];
var vertexOffset = 0;
glObjects.forEach(function pushingObject(o){
if (vertexOffset + o.vertices.length > 65536){
vertexOffset = 0;
glDraw();
data.vertices.length = 0;
data.uv.length = 0;
data.triangles.length = 0;
}
data.vertices.push.apply(data.vertices,o.vertices);
data.uv.push.apply(data.uv,o.uv);
o.triangles.forEach(function pushingTriangles(index){
data.triangles.push(index+vertexOffset);
});
vertexOffset += o.vertices.length/3; // change to component length later
});
glDraw();
function glDraw(){
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data.vertices),gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data.uv),gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, triangleBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(data.triangles), gl.STATIC_DRAW);
gl.drawElements(gl.TRIANGLES, data.triangles.length, gl.UNSIGNED_SHORT, 0);
}
};
But this isnt fast enough because as I learnt, passing in new bufferData is slow. So my question is, what does one do in this situation? I was unable to locate any webgl resource that deal with this. My feeling leans towards creating multiple VBO objects but I want to make sure I am going in the right direction first. And as a follow up question, suppose if one need to draw many cubes all with unique position (x,y,z) and orientation (rX,rY,rZ), how does one go about implementing it? Thanks in advance.
Ok I solved my problem and I'll leave this here for stragglers:
Basically, I had the right idea in that I need to use multiple draw calls as each indexed draw (drawElements) can only refer to 2^16 elements in a VBO. The flaw in my first implementation is that I actually tried to reconstruct a new big typedArray made of multiple cube vertices in every render call. Needless to say, that is very slow. So instead of that, I really should have only created the typedArray/buffer once. To overcome the 2^16 element reference limitation, all I have to do is to separate the one bigass typedArray into manageable sizes, and this is exactly what this new version of pushData does:
data.pushData = function(){
// ensure each vertex attribute has less than 2^16 vertices because that is how many that be be referenced each time
// with gl.drawElements call
function newChunk(){
return {
vertices: [],
uv: [],
triangles: []
}
}
var chunk = newChunk();
var vertexOffset = 0;
glObjects.forEach(function pushingVerts(o){
if (vertexOffset + o.vertices.length > 65536){
vertexOffset = 0;
data.chunks.push(chunk);
chunk = newChunk();
}
chunk.vertices.push.apply(chunk.vertices,o.vertices);
chunk.uv.push.apply(chunk.uv,o.uv);
o.triangles.forEach(function pushingTriangles(index){
chunk.triangles.push(index+vertexOffset);
});
vertexOffset += o.vertices.length/3; // change to component length later
});
data.chunks.push(chunk);
data.chunks.forEach(function toTypeArray(c){
c.vertices = new Float32Array(c.vertices);
c.uv = new Float32Array(c.uv);
c.triangles = new Uint16Array(c.triangles);
});
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, sizeofFloat * 65536*3,gl.DYNAMIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
gl.bufferData(gl.ARRAY_BUFFER, sizeofFloat * 65536*2,gl.DYNAMIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, triangleBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, sizeofFloat * 65536, gl.DYNAMIC_DRAW);
// for some reason only allocating sizeofUnsignedShort * 65536 is not enough.
return data.chunks;
};
Then for render its simply:
data.renderChunks = function(){
data.chunks.forEach(function renderChunk(c){
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, c.vertices);
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, c.uv);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, triangleBuffer);
gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, c.triangles);
gl.drawElements(gl.TRIANGLES, c.triangles.length, gl.UNSIGNED_SHORT, 0);
});
};
Also I changed from using gl.bufferData to gl.bufferSubData to avoid the overhead of constructing a new buffer.
And with this I can now draw 60,000 cubes (at least):
http://jsfiddle.net/n5fjhe21/1/
I created a simple scene with a cube moving parallel to the x-axis. Everything works as expected until I rotate the camera around the y-axis. Then the cube follows this rotation and moves parallel to the screen (x-axis in camera coordinates).
Again the initial setup:
Camera at [0, 2, 10] looking at [0, 0, 0]
Cube initially placed at [0, 0, 0], moving along the x-axis between [-10, 10]
Why does my camera movement affect the orientation of the cube?
Here is some of the relevant code. I you would like to see more, don't hesitate to ask. I am using glMatrix for vector and matrix operations.
Main drawing routine:
// Clear the canvas before we start drawing on it.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Use the full window (minus border)
canvas.width = window.innerWidth - 16;
canvas.height = window.innerHeight - 16;
// Set viewport
gl.viewport(0, 0, canvas.width, canvas.height);
// Reset the perspective matrix
cam.aspectRatio = canvas.width / canvas.height;
mat4.perspective(perspectiveMatrix, cam.fovy, cam.aspectRatio, cam.nearPlane, cam.farPlane);
// Create the mvMatrix
mat4.lookAt(mvMatrix, cam.position, cam.poi, cam.up);
// Draw all objects
for (i = 0; i < ObjectStack.length; i++) {
ObjectStack[i].draw();
}
Camera rotation:
// Rotation via yaw and pitch (FPS-style)
this.rotateYP = function (yaw, pitch) {
// Rotation speed
var rotSpeed = 0.5;
yaw *= rotSpeed;
pitch *= rotSpeed;
// Update rotation
var quatYaw = quat.create();
quat.setAxisAngle(quatYaw, this.up, degToRad(yaw));
var quatPitch = quat.create();
quat.setAxisAngle(quatPitch, this.right, degToRad(pitch));
var quatCombined = quat.create();
quat.multiply(quatCombined, quatYaw, quatPitch);
// Update camera vectors
var tmp = vec3.create();
vec3.subtract(tmp, this.poi, this.position);
vec3.transformQuat(tmp, tmp, quatCombined);
vec3.add(tmp, this.position, tmp);
this.setPOI(tmp);
};
My setPOI() method (POI = point of interest):
this.setPOI = function (poi) {
// Set new poi
vec3.copy(this.poi, poi);
// Set new view vector
vec3.subtract(this.view, poi, this.position);
vec3.normalize(this.view, this.view);
// Set new right vector
vec3.cross(this.right, this.view, [0.0, 1.0, 0.0]);
vec3.normalize(this.right, this.right);
// Set new up vector
vec3.cross(this.up, this.right, this.view);
vec3.normalize(this.up, this.up);
};
Object draw method for the cube:
this.draw = function () {
// Save current mvMatrix
mvPushMatrix();
// Object movement
mat4.translate(mvMatrix, mvMatrix, position);
// Object rotation
//mat4.mul(mvMatrix, mvMatrix, orientation);
// Object scaling
// ...
// Set shader
setShader();
// Bind the necessary buffers
gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);
gl.vertexAttribPointer(positionAttribute, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, normalsBuffer);
gl.vertexAttribPointer(normalAttribute, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.vertexAttribPointer(texCoordAttribute, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);
// Set active texture
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, cubeTexture);
gl.uniform1i(gl.getUniformLocation(ShaderStack[shader], "uSampler"), 0);
// Send the triangles to the graphics card for drawing
gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);
gl.bindTexture(gl.TEXTURE_2D, null);
// Clean up the changed mvMatrix
mvPopMatrix();
};
And finally the setShader() used above:
function setShader() {
var shaderProgram = ShaderStack[shader];
gl.useProgram(shaderProgram);
var pUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
gl.uniformMatrix4fv(pUniform, false, perspectiveMatrix);
var mvUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
gl.uniformMatrix4fv(mvUniform, false, mvMatrix);
var normalMatrix = mat4.create();
mat4.invert(normalMatrix, mvMatrix);
mat4.transpose(normalMatrix, normalMatrix);
var nUniform = gl.getUniformLocation(shaderProgram, "uNormalMatrix");
gl.uniformMatrix4fv(nUniform, false, normalMatrix);
normalAttribute = gl.getAttribLocation(shaderProgram, "aVertexNormal");
gl.enableVertexAttribArray(normalAttribute);
positionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(positionAttribute);
texCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord");
gl.enableVertexAttribArray(texCoordAttribute);
};
Sorry for posting all this code here. If you have any idea, please let me know!
I suspect you answered your question in your own question:
a simple scene with a cube moving parallel to the x-axis ... Then the cube follows this rotation and moves parallel to the screen (x-axis in camera coordinates).
Something like this happening leads me to believe that you applied the translation operation to your model-view matrix, not your model matrix, and from your code, I think I am right:
mat4.translate(mvMatrix, mvMatrix, position);
To fix this, you'll want to separate out your model and your view matrix, apply the translation to your model matrix, and then multiply the result by your view. Let me know how it goes!
If you're still confused by matrices, give this a read:
http://solarianprogrammer.com/2013/05/22/opengl-101-matrices-projection-view-model/
I need to implement what amounts to a "Google Maps" style zoom effect in WebGL. Specifically I have a simple 2-dimensional scene that is always perpendicular to the camera. When a user clicks on the scene, the camera should zoom to a location that is over the click, but closer to the 2-dimensional scene.
For example see this jsfiddle, that implements the scene but no zooming:
http://jsfiddle.net/JqBs8/4/
If you have a WebGL enabled browser, you should see a triangle and a square (2-dimensional) rendered at -7 on the Z axis. I have put in a placeholder handleMouseUp() event handler that logs any click events, but I'm a little lost as to how to translate the coordinates given by the click event into a new location for the camera (or I guess equivalently a new view matrix).
(The jsfiddle is based on tutorial 1 from learningwebgl.com and uses the glMatrix http://code.google.com/p/glmatrix/ library for matrix operations. Keep in mind that this is WebGL, which is similar to OpenGL ES, and has no access to glu* functions.)
I've written something in this jsfiddle that should help you.
http://jsfiddle.net/hedzys6r/
(or https://codepen.io/brainjam/pen/gBZyGm)
Just click on the WebGL window to zoom in to where the mouse is pointing.
The basic idea is that a point in the WebGL window is obtained by projecting it from 3-space using the projection matrix pMatrix and the view matrix (the view matrix depends on where the camera is and the direction in which it is looking). The composition of these matrices is named pvMatrix in the code.
If you want the opposite transform from the window back to three space, you have to take a clip space coordinate (x,y,z) and 'unproject' it back into 3D using the inverse of pvMatrix. In clip space, coordinates are in the range [-1,1], and the z coordinate is depth.
In the OpenGL world, these transforms are implemented in gluProject() and gluUnproject() (which you can Google for more information and source code).
In the jsfiddle example, we calculate the (x,y) coordinates in clip space, and then unproject (x,y,z) for two different values of z. From that we get two points in 3D space that map onto (x,y), and we can infer a direction vector. We can then move the camera in that direction to get a zoom effect.
In the code, the camera position is at the negation of the eye vector.
This example shows you how to move the camera in the direction that you are clicking. If you want to actually move towards specific objects in the scene, you have to implement something like object selection, which is a different kettle of fish. The example I've given is unaware of the objects in the scene.
This is really part of brainjam's answer, but just in case that jsfiddle were to go away, I wanted to make sure the code was archived. Here is the primary bit:
function handleMouseUp(event) {
var world1 = [0,0,0,0] ;
var world2 = [0,0,0,0] ;
var dir = [0,0,0] ;
var w = event.srcElement.clientWidth ;
var h = event.srcElement.clientHeight ;
// calculate x,y clip space coordinates
var x = (event.offsetX-w/2)/(w/2) ;
var y = -(event.offsetY-h/2)/(h/2) ;
mat4.inverse(pvMatrix, pvMatrixInverse) ;
// convert clip space coordinates into world space
mat4.multiplyVec4(pvMatrixInverse, [x,y,-1,1], world1) ;
vec3.scale(world1,1/world1[3]) ;
mat4.multiplyVec4(pvMatrixInverse, [x,y,0,1], world2) ;
vec3.scale(world2,1/world2[3]) ;
// calculate world space view vector
vec3.subtract(world2,world1,dir) ;
vec3.normalize(dir) ;
vec3.scale(dir,0.3) ;
// move eye in direction of world space view vector
vec3.subtract(eye,dir) ;
drawScene();
console.log(event)
}
And the entirety of the JS...
var gl;
function initGL(canvas) {
try {
gl = canvas.getContext("experimental-webgl");
gl.viewportWidth = canvas.width;
gl.viewportHeight = canvas.height;
} catch (e) {
}
if (!gl) {
alert("Could not initialise WebGL, sorry :-(");
}
}
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");
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
}
var mvMatrix = mat4.create();
var pMatrix = mat4.create();
function setMatrixUniforms() {
gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix);
gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix);
}
var triangleVertexPositionBuffer;
var squareVertexPositionBuffer;
function initBuffers() {
triangleVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
var vertices = [
0.0, 1.0, 0.0,
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
triangleVertexPositionBuffer.itemSize = 3;
triangleVertexPositionBuffer.numItems = 3;
squareVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
vertices = [
1.0, 1.0, 0.0,
-1.0, 1.0, 0.0,
1.0, -1.0, 0.0,
-1.0, -1.0, 0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
squareVertexPositionBuffer.itemSize = 3;
squareVertexPositionBuffer.numItems = 4;
}
var eye = vec3.create([0,0,0]) ; // negation of actual eye position
var pvMatrix = mat4.create();
var pvMatrixInverse = mat4.create();
function drawScene() {
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);
mat4.identity(mvMatrix);
// calculate the view transform mvMatrix, and the projection-view matrix pvMatrix
mat4.translate(mvMatrix, eye);
mat4.multiply(pMatrix,mvMatrix,pvMatrix) ;
mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
setMatrixUniforms();
gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);
mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
setMatrixUniforms();
gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
}
function handleMouseUp(event) {
var world1 = [0,0,0,0] ;
var world2 = [0,0,0,0] ;
var dir = [0,0,0] ;
var w = event.srcElement.clientWidth ;
var h = event.srcElement.clientHeight ;
// calculate x,y clip space coordinates
var x = (event.offsetX-w/2)/(w/2) ;
var y = -(event.offsetY-h/2)/(h/2) ;
mat4.inverse(pvMatrix, pvMatrixInverse) ;
// convert clip space coordinates into world space
mat4.multiplyVec4(pvMatrixInverse, [x,y,-1,1], world1) ;
vec3.scale(world1,1/world1[3]) ;
mat4.multiplyVec4(pvMatrixInverse, [x,y,0,1], world2) ;
vec3.scale(world2,1/world2[3]) ;
// calculate world space view vector
vec3.subtract(world2,world1,dir) ;
vec3.normalize(dir) ;
vec3.scale(dir,0.3) ;
// move eye in direction of world space view vector
vec3.subtract(eye,dir) ;
drawScene();
console.log(event)
}
function webGLStart() {
var canvas = document.getElementById("lesson01-canvas");
initGL(canvas);
initShaders();
initBuffers();
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.enable(gl.DEPTH_TEST);
canvas.onmouseup = handleMouseUp;
drawScene();
}
webGLStart();