adamdaly adamdaly - 19 days ago 8
Javascript Question

webgl, texture coordinates and obj

I'm finding it difficult to understand the correlation between vertex and texture coordinates when the data is rendered. I have a cube being drawn using drawElements form data parsed from an obj. I got textures somewhere close to working with a simple plane where the number of vertex for position and for texture coordinates but once i use a more complex model or even just a more complex uv unwrap i end up with the texture going all wrong.

From what i've read there doesn't seen to be a way of using texture coordinate indices the same way you would for vertex position, which is unfortunate because the obj has that information. The way i've gotten it close to working was by building an array of texture coordinates from the indices data in the obj. But because the length of the vertex and texture coordinate arrays differ (for example in an obj for a cube there are 8 vertex and up to 36 texture coordinate depending on have the mesh is unwrapped) they don't correlate.

What is the correct workflow for using drawElements and mapping the vertex to its correct texture coordinates.

Answer

You are correct, you can not easily use different indices for different attributes (in your case positions and texture coordinates).

A common example is a cube. If you want to render a cube with lighting you need normals. There are only 8 positions on a cube but each face of the cube needs 3 different normals for the same positions, one normal for each face that shares that position. That means you need 24 vertices total, 4 for each of the 6 faces of the cube.

If you have a file format that has separate indices for different attributes you'll need to expand them out so that each unique combination of attributes (position, normal, texture coord, etc..) is in your buffers.

Most game engines would do this kind of thing offline. In other words, they'd write some tool that reads the OBJ file, expands the various attributes, and then writes the data back out pre-expanded. That's because generating the expanded data can be time consuming at runtime for a large model if you're trying to optimize the data and only keep unique vertices.

If you don't care about optimal data then just expand based on the indices. The number of indices for each type of attribute should be the same.

Note: positions are not special. I bring this up because you said there doesn't seen to be a way of using texture coordinate indices the same way you would for vertex position. WebGL has no concept of "positions". It just has attributes which describe how to pull data out of buffers. What's in those attributes (positions, normals, random data, whatever), is up to you. gl.drawElements indexes the entire combination of attributes you supply. If you pass in an index of 7 it's going to give you element 7 of each attribute.

Note that the above is describing how pretty much all 3d engines written in WebGL work. That said you can get creative if you really want to.

Here's a program that stores positions and normals in textures. It then puts the indices in buffers. Because textures are random access it can therefore have different indices for positions and normals

var countElem = document.getElementById("t");
var canvas = document.getElementById("c");
var gl = canvas.getContext("webgl");
var ext = gl.getExtension("OES_texture_float");
if (!ext) {
    alert("need OES_texture_float extension cause I'm lazy");
    //return;
}
if (gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS) < 2) {
    alert("need to be able to access textures from vertex shaders");
    //return;
}

var m4 = twgl.m4;
var v3 = twgl.v3;
var programInfo = twgl.createProgramInfo(gl, ["vshader", "fshader"]);

// Cube data
var positions = [
    -1, -1, -1, // 0 lbb
    +1, -1, -1, // 1 rbb      2---3
    -1, +1, -1, // 2 ltb     /|  /|
    +1, +1, -1, // 3 rtb    6---7 |
    -1, -1, +1, // 4 lbf    | | | |
    +1, -1, +1, // 5 rbf    | 0-|-1
    -1, +1, +1, // 6 ltf    |/  |/
    +1, +1, +1, // 7 rtf    4---5
];
var positionIndices = [
  3, 7, 5, 3, 5, 1, // right
  6, 2, 0, 6, 0, 4, // left
  6, 7, 3, 6, 3, 2, // top
  0, 1, 5, 0, 5, 4, // bottom
  7, 6, 4, 7, 4, 5, // front
  2, 3, 1, 2, 1, 0, // back
];
var normals = [
 +1,  0,  0,
 -1,  0,  0,
  0, +1,  0,
  0, -1,  0,
  0,  0, +1,
  0,  0, -1,
]
var normalIndices = [
  0, 0, 0, 0, 0, 0,  // right
  1, 1, 1, 1, 1, 1,  // left
  2, 2, 2, 2, 2, 2,  // top
  3, 3, 3, 3, 3, 3,  // bottom
  4, 4, 4, 4, 4, 4,  // front
  5, 5, 5, 5, 5, 5,  // back
];

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

function createIndices(data, indices) {
  // scale indices into texture coordinates
  var scaledIndices = new Float32Array(indices.length);

  // to index the value in the texture we need to
  // compute a texture coordinate that will access
  // the correct texel. To do that we need access from
  // the middle of the first texel to the middle of the
  // last texel.
  //
  // In other words if we had 3 values (and therefore
  // 3 texels) we'd have something like this
  //
  //     ------3x1 ----- texels ----------
  //     [         ][         ][         ]
  // 0.0 |<----------------------------->| 1.0
  //
  // If we just did index / numValues we'd get
  //
  //     [         ][         ][         ]
  //     |          |          |
  //     0.0       0.333       0.666
  //
  // Which is right between texels so we add a
  // a halfTexel to get this
  //
  //     [         ][         ][         ]
  //          |          |          |
  //        0.167       0.5       0.833
  //

  var size = data.length / 3;
  var texel = 1 / size;
  var halfTexel = texel / 2;
  for (var ii = 0; ii < indices.length; ++ii) {
      scaledIndices[ii] = indices[ii] / size + halfTexel;
  }
  return scaledIndices;
}

var bufferInfo = twgl.createBufferInfoFromArrays(gl, {
  a_positionIndex: { size: 1, data: createIndices(positions, positionIndices) },
  a_normalIndex: { size: 1, data: createIndices(normals, normalIndices), },
});
                                                 
var textures = twgl.createTextures(gl, {
  positions: { 
    format: gl.RGB, 
    type: gl.FLOAT,
    height: 1, 
    src: positions, 
    min: gl.NEAREST,
    mag: gl.NEAREST,
    wrap: gl.CLAMP_TO_EDGE,
  },
  normals: { 
    format: gl.RGB, 
    type: gl.FLOAT,
    height: 1, 
    src: normals, 
    min: gl.NEAREST,
    mag: gl.NEAREST,
    wrap: gl.CLAMP_TO_EDGE,
  },
});
                                     
var xRot = degToRad(30);
var yRot = degToRad(20);

var lightDir = v3.normalize([-0.2, -0.1, 0.5]);

function draw(time) {
  time *= 0.001;  // convert to seconds
  
  twgl.resizeCanvasToDisplaySize(gl.canvas);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  
  yRot = time;

  gl.enable(gl.DEPTH_TEST);
  gl.enable(gl.CULL_FACE);

  gl.useProgram(programInfo.program);

  var persp = m4.perspective(
    degToRad(45),
    gl.canvas.clientWidth / gl.canvas.clientHeight,
    0.1, 100.0);

  var mat = m4.identity();
  mat = m4.translate(mat, [0.0, 0.0, -5.0]);
  mat = m4.rotateX(mat, xRot);
  mat = m4.rotateY(mat, yRot);

  var uniforms = {
    u_positions: textures.positions,
    u_normals: textures.normals,
    u_mvpMatrix: m4.multiply(persp, mat),
    u_mvMatrix: mat,
    u_color: [0.5, 0.8, 1, 1],
    u_lightDirection: lightDir,
  };

  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  twgl.setUniforms(programInfo, uniforms);
  twgl.drawBufferInfo(gl, bufferInfo);
  
  requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="//twgljs.org/dist/2.x/twgl-full.min.js"></script>
<script id="vshader" type="whatever">
attribute float a_positionIndex;
attribute float a_normalIndex;
attribute vec4 a_pos;

uniform sampler2D u_positions;
uniform sampler2D u_normals;
uniform mat4 u_mvpMatrix;
uniform mat4 u_mvMatrix;

varying vec3 v_normal;

void main() {
  vec3 position = texture2D(
      u_positions, vec2(a_positionIndex, 0.5)).rgb;
  vec3 normal = texture2D(
      u_normals, vec2(a_normalIndex, 0.5)).rgb;
  gl_Position = u_mvpMatrix * vec4(position, 1);
  v_normal = (u_mvMatrix * vec4(normal, 0)).xyz;
}
</script>
<script id="fshader" type="whatever">
precision mediump float;

uniform vec4 u_color;
uniform vec3 u_lightDirection;

varying vec3 v_normal;

void main() {
  float light = dot(
      normalize(v_normal), u_lightDirection) * 0.5 + 0.5;
  gl_FragColor = vec4(u_color.rgb * light, u_color.a);
}
</script>
<canvas id="c"></canvas>