Android geometri
Hvis vi betrakter denne figuren, ser vi at den kan settes sammen av noen få basisformer: sylinder, sirkelflate og halvkule.
Vi baserer oss på dette når vi skal produsere figuren.
Vi lager oss en klasse for hver av de tre grunnformene. Når klassen initialiseres lager vi de bufferdata vi trenger.
Når figuren tegnes, kopler vi til dataene og sender dem til vertex-fragmentet. Klassen for en sylinder ser slik ut:
function Cylinder(R,H,N)
{
// produce a cylinder without ends
var vertices=[]; //each points coordinates
var normals=[]; //each points normal
var indices=[]; //each drawobjects index to points (Gl_TRIANGLE_STRIPS)
// calculate
var dw=2*Math.PI/(1.0*N);
var dh=H/(1.0*N);
var w=0.0;
var h=0.0;
var index=0;
// along the length
while(h < H+0.0001)
{
w=0.0;
// round the cylinder
while(w < 2*Math.PI+0.0001)
{
vertices=vertices.concat([R*Math.cos(w),R*Math.sin(w),h]);
normals=normals.concat([R*Math.cos(w),R*Math.sin(w),0]);
vertices=vertices.concat([R*Math.cos(w),R*Math.sin(w),h+dh]);
normals=normals.concat([R*Math.cos(w),R*Math.sin(w),0]);
indices=indices.concat(index++);
indices=indices.concat(index++);
w+=dw;
}
h+=dh;
}
// prepare buffers once and for all
this.verticesBuffer= gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.verticesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
this.verticesNormalBuffer= gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.verticesNormalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
this.verticesIndexBuffer= gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.verticesIndexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
this.verticesIndexBuffer.COUNT=indices.length;
this.draw=function()
{
gl.bindBuffer(gl.ARRAY_BUFFER,this.verticesBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute,3,gl.FLOAT,false,0,0);
// Set the normal attribute for the vertices.
gl.bindBuffer(gl.ARRAY_BUFFER,this.verticesNormalBuffer);
gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute,3,gl.FLOAT,false,0,0);
// Draw the cylinder.
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.verticesIndexBuffer);
gl.drawElements(gl.TRIANGLE_STRIP,this.verticesIndexBuffer.COUNT,gl.UNSIGNED_SHORT,0);
}
}
For en halvkule (hat)
function Hat(R,N)
{
// produce a half sphere
var vertices=[]; //each points coordinates
var normals=[]; //each points normal
var indices=[]; //each drawobjects index to points (Gl_TRIANGLE_STRIPS)
// calculate
var dw=2*Math.PI/(1.0*N);
var dv=(0.5)*Math.PI/(1.0*N);
var w=0.0;
var index=0;
var v=0.0;
// outer loop
while(v < (0.5)*Math.PI+0.0001)
{
w=0.0;
// inner loop, round the hat
while(w < 2*Math.PI+0.0001)
{
vertices=vertices.concat([R*Math.sin(w)*Math.cos(v),
R*Math.cos(w)*Math.cos(v),
R*Math.sin(v)]);
normals=normals.concat( [R*Math.sin(w)*Math.cos(v),
R*Math.cos(w)*Math.cos(v),
R*Math.sin(v)]);
vertices=vertices.concat([R*Math.sin(w)*Math.cos(v+dv),
R*Math.cos(w)*Math.cos(v+dv),
R*Math.sin(v+dv)]);
normals=normals.concat( [R*Math.sin(w)*Math.cos(v+dv),
R*Math.cos(w)*Math.cos(v+dv),
R*Math.sin(v+dv)]);
indices=indices.concat(index++);
indices=indices.concat(index++);
w+=dw;
}
v+=dv;
}
// prepare buffers once and for all
this.verticesBuffer= gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.verticesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
this.verticesNormalBuffer= gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.verticesNormalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
this.verticesIndexBuffer= gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.verticesIndexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
this.verticesIndexBuffer.COUNT=indices.length;
this.draw=function()
{
gl.bindBuffer(gl.ARRAY_BUFFER, this.verticesBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
// Set the normal attribute for the vertices.
gl.bindBuffer(gl.ARRAY_BUFFER, this.verticesNormalBuffer);
gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, 3, gl.FLOAT, false, 0, 0);
// Draw the hat.
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.verticesIndexBuffer);
gl.drawElements(gl.TRIANGLE_STRIP, this.verticesIndexBuffer.COUNT, gl.UNSIGNED_SHORT, 0);
}
}
For en sirkelflate, en disk
function Disk(R,N)
{
// produce a simple disk
var vertices=[]; //each points coordinates
var normals=[]; //each points normal
var indices=[]; //each drawobjects index to points (Gl_TIANGLE_FAN)
var dw=2*Math.PI/(1.0*N);
var w=0.0;
var index=0;
vertices=vertices.concat([0.0,0.0,0.0]);
normals=normals.concat([0.0,0.0,1.0]);
indices=indices.concat(index++);
while(w < 2*Math.PI+0.0001)
{
vertices=vertices.concat([R*Math.cos(w),R*Math.sin(w),0.0]);
normals=normals.concat([0.0,0.0,1.0]);
indices=indices.concat(index++);
w+=dw;
}
// prepare buffers once and for all
this.verticesBuffer= gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.verticesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
this.verticesNormalBuffer= gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.verticesNormalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
this.verticesIndexBuffer= gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.verticesIndexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
this.verticesIndexBuffer.COUNT=indices.length;
this.draw=function()
{
gl.bindBuffer(gl.ARRAY_BUFFER, this.verticesBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
// Set the normal attribute for the vertices.
gl.bindBuffer(gl.ARRAY_BUFFER, this.verticesNormalBuffer);
gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, 3, gl.FLOAT, false, 0, 0);
// Draw the disk.
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.verticesIndexBuffer);
gl.drawElements(gl.TRIANGLE_FAN, this.verticesIndexBuffer.COUNT, gl.UNSIGNED_SHORT, 0);
}
}
Javascript
Javascriptet som handterer vår tegning er inkludert som egen fil. De viktigste delene av denne
koden er kommentert nedenfor. Hele fila ser slik ut:
Det kan være lurt å kikke på OpenGL ES 2.0 Reference Pages
[2]
for å få en forklaring av
de enkelte metodene.
Følgende globale variable er definert.
var canvas;
var gl;
var mvMatrix;
var perspectiveMatrix;
var shaderProgram;
// sizes
var bodyLength=3.0;
var bodyRadius=1.5;
var limbLength=1.7;
var limbRadius=0.4;
// drawing precision
var Precision=30;
//shapes
var aCylinder;
var aDisk;
var aHat;
var aSmallCylinder;
var aSmallDisk;
var aSmallHat;
// animation
var sceneRotation = 0.0;
var lastUpdateTime = 0;
var armswing=0.0;
var deltaArmswing=1.5;
var maxArmswing=50.0;
var talkSwing=0.0;
var talkCount=0;
var deltaTalkCount=1;
Funkjsonen start() kalles typisk enten ved body onload, eller ved et script i bunnen (etter canvas) i selve
websiden. I dette eksempelet er det denne funksjone som drar hele jobben.
function start() {
canvas = document.getElementById("glcanvas");
initWebGL(canvas);
if (gl) {
gl.clearColor(1.0, 1.0, 1.0, 1.0);
gl.clearDepth(1.0);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
initBuffers();
initShaders();
gl.clearColor(1.0, 1.0, 1.0, 1.0);
gl.enable(gl.DEPTH_TEST);
tick();
}
}
Funksjonen initWebGL() forsøker å sette opp WebGL i canvas-elementet. Merk at konstanten
"experimental-webgl" skal bytte sut med "webgl" etterhvert som denne teknologien modnes hos nettleserne.
function initWebGL() {
gl = null;
// browser and veesions reckognize webgl differently ?
var names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
for(var i = 0; i < names.length; i++){
try {gl = canvas.getContext("experimental-webgl");}
catch(e) {}
if(gl){break;}
}
if (!gl) {
alert("Unable to initialize WebGL. Your browser may not support it.");
}
}
I funksjonen initBuffers() setter vi opp de punktene som beskriver modellen vår
og som etterhvert skal sendes til vertex-shaderen.
function initBuffers() {
// set up all shapes
aCylinder=new Cylinder(bodyRadius,bodyLength,Precision);
aDisk=new Disk(bodyRadius,Precision);
aHat=new Hat(bodyRadius,Precision);
aSmallCylinder=new Cylinder(limbRadius,limbLength,Precision);
aSmallDisk=new Disk(limbRadius,Precision);
aSmallHat=new Hat(limbRadius,Precision);
}
Funksjonen drawScene() skiller seg fra det vi kjenner fra tradisjonell OpenGL på
den måten at vi ikke lenger "tegner" hjørne for hjørne.
I stedet sender vi en punktliste til shaderen, sammen med de to matrisene
(modelview og perspective). Deretter starter vi tegningen med gl.drawArrays
function drawScene() {
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
perspectiveMatrix =
makePerspective(45, canvas.width / canvas.height, 0.1, 100.0);
loadIdentity();
// Move the drawing a bit from default eye-pos
mvTranslate([-0.0, 0.0, -16.0]);
mvRotate(sceneRotation, [1, 0, 1]);
mvPushMatrix();
// Draw the main body.
//---------------
setMatrixUniforms();
aCylinder.draw();
// Draw the disk in cylinder ends
aDisk.draw();
mvTranslate([0.0, 0.0, bodyLength]);
setMatrixUniforms();
aDisk.draw();
// draw head top
// talk
mvRotate(talkSwing,[1.0, 0.0, 0.0]);
mvTranslate([0.0, 0.0, 0.07+0.05*talkSwing]);
setMatrixUniforms();
aDisk.draw();
aHat.draw();
// Draw arms
//-------------
mvPopMatrix();
mvPushMatrix();
mvTranslate([bodyRadius+limbRadius+0.1,0.0,bodyLength-limbRadius]);
drawLimb(-armswing)
mvPopMatrix();
mvPushMatrix();
mvTranslate([-(bodyRadius+limbRadius+0.1),0.0,bodyLength-limbRadius]);
drawLimb(armswing);
// Draw legs
//-----------
mvPopMatrix();
mvPushMatrix();
mvTranslate([(bodyRadius-limbRadius*2),0.0,0.2]);
drawLimb(armswing);
mvPopMatrix();
mvPushMatrix();
mvTranslate([-(bodyRadius-limbRadius*2),0.0,0.2]);
drawLimb(-armswing);
mvPopMatrix();
// Update the rotation for the next draw
var currentTime = (new Date).getTime();
if (lastUpdateTime) {
var delta = currentTime - lastUpdateTime;
sceneRotation += (30 * delta) / 1000.0;
}
lastUpdateTime = currentTime;
armswing=armswing+deltaArmswing;
if ((armswing > maxArmswing)||(armswing < -maxArmswing))
deltaArmswing=-deltaArmswing;
talkCount=talkCount+deltaTalkCount;
talkSwing=0.2*talkCount;
if(talkCount > 40)
deltaTalkCount=-1;
else if(talkCount <1)
deltaTalkCount=1;
}
Hjelpefunksjonen drawLimb er definert slik:
function drawLimb(swing)
{
// arm or a leg
pushMatrix();
mvRotate(swing,[1.0,0.0,0.0]);
setMatrixUniforms();
aSmallHat.draw();
popMatrix();
pushMatrix();
mvRotate(180.0+swing,[1.0,0.0,0.0]);
setMatrixUniforms();
aSmallCylinder.draw();
mvTranslate([0.0,0.0,limbLength]);
setMatrixUniforms();
aSmallHat.draw();
popMatrix();
}
Funksjonen setMatrixUniforms() setter status på transformasjonsmatriser til shaderen.
function setMatrixUniforms() {
// set al the transformation matrices (ModelView, perspective and normal
var pUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
gl.uniformMatrix4fv(pUniform, false, new Float32Array(perspectiveMatrix.flatten()));
var mvUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
gl.uniformMatrix4fv(mvUniform, false, new Float32Array(mvMatrix.flatten()));
var normalMatrix = mvMatrix.inverse();
normalMatrix = normalMatrix.transpose();
var nUniform = gl.getUniformLocation(shaderProgram, "uNormalMatrix");
gl.uniformMatrix4fv(nUniform, false, new Float32Array(normalMatrix.flatten()));
}
De to funksjonene initShaders() og getShader() laster inn shaderne, kompilerer dem og etablerer shaderprogrammet.
function initShaders() {
var fragmentShader = getShader(gl, "shader-fs");
var vertexShader = getShader(gl, "shader-vs");
// Create the shader program
shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
// ok?
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert("Unable to initialize the shader program.");
}
gl.useProgram(shaderProgram);
// connect shader attribute to buffers.
// let the shaderprogram remember the addresses
// so we can use when we fill the attribute buffers
shaderProgram.vertexPositionAttribute =
gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
shaderProgram.vertexNormalAttribute =
gl.getAttribLocation(shaderProgram, "aVertexNormal");
gl.enableVertexAttribArray(shaderProgram.vertexNormalAttribute);
}
function getShader(gl, id) {
var shaderScript = document.getElementById(id);
// ok?
if (!shaderScript) {
return null;
}
// Building the shader source string.
var theSource = "";
var currentChild = shaderScript.firstChild;
while(currentChild) {
if (currentChild.nodeType == 3) {
theSource += currentChild.textContent;
}
currentChild = currentChild.nextSibling;
}
// What type of shader, based on its MIME type.
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; // Unknown shader type
}
// Send the source to the shader object
gl.shaderSource(shader, theSource);
// Compile the shader program
gl.compileShader(shader);
// See if it compiled successfully
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert("An error occurred compiling the shaders: " + gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
Fragment shader
Fragment shaderen arbeider med en farge og en lysretning. Begge er satt
fra vertex shader. Merk at de har kvalifiseringen varying.
varying mediump vec3 vLighting;
varying lowp vec4 vColor;
void main(void) {
gl_FragColor = vec4(vColor.rgb * vLighting, vColor.a);
}
Vertex shader
De tre variablene: uMVMatrix, uPMatrix og uNormalMatrix blir satt fra
Javascriptkoden. aVertexPosition representerer det aktuelle punktet, slik det er ordnet
i en buffer fra Javascriptet og aVertexNormal er normalen i punktet.
Merk at hele lyssettingen etableres i shaderen.
attribute mediump vec3 aVertexNormal; // or in
attribute mediump vec3 aVertexPosition;
uniform mediump mat4 uNormalMatrix;
uniform mediump mat4 uMVMatrix;
uniform mediump mat4 uPMatrix;
varying lowp vec4 vColor;
varying mediump vec3 vLighting;
void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
// Lighting effect
mediump vec3 ambientLight = vec3(0.6, 0.6, 0.6);
mediump vec3 directionalLightColor = vec3(0.5, 0.5, 0.75);
mediump vec3 directionalVector = vec3(0.85, 0.8, 0.75);
mediump vec4 transformedNormal = uNormalMatrix * vec4(aVertexNormal, 1.0);
mediump float directional = max(dot(transformedNormal.xyz, directionalVector), 0.0);
vLighting = ambientLight + (directionalLightColor * directional);
// set color
vColor=vec4(0.0, 1.0, 0.0, 1.0);
}
Dette er resultatet:
Ikke veldig imponerende, men vi har fått basisgeometrien på plass. I den neste modulen skal vi
forsøke å få litt orden på materialer, og vi skal få med øyne og "følehorn".
Du kan også inspisere resultatet og kildekoden på en enklere side:
index.html
http://www.it.hiof.no/~borres/dw/wgl/android/index.html