-
-
Save hdon/3dced04d50ab678a7e2d to your computer and use it in GitHub Desktop.
OpenGL renderer for MD5 mesh+animation in D with GPU vertex skinning
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module ants.md5; | |
import file = std.file; | |
import std.string; | |
import std.conv; | |
import std.algorithm : map, appender; | |
import std.exception : enforce; | |
import std.path : dirName; | |
import derelict.opengl3.gl3; | |
import gl3n.linalg : Matrix, Vector, Quaternion, cross; | |
import gl3n.interpolate : lerp; | |
import ants.vertexer; | |
import ants.material; | |
import ants.shader; | |
import ants.texture; | |
import ants.gametime; | |
import std.math : sqrt; | |
import std.stdio : writeln, writefln; | |
private alias Vector!(double, 3) vec3; | |
private alias Vector!(double, 2) vec2; | |
private alias Vector!(float, 2) vec2f; | |
private alias Vector!(float, 3) vec3f; | |
private alias Vector!(float, 4) vec4f; | |
alias Matrix!(double, 3, 3) mat3; | |
alias Matrix!(double, 4, 4) mat4; | |
alias Quaternion!(double) quat; | |
private Vertexer vertexer; | |
private Material emptyMaterial; | |
private ShaderProgram shaderProgram; | |
private ShaderProgram shaderProgram1; | |
private ShaderProgram md5ShaderProgram; | |
private ShaderProgram varyingColorShaderProgram; | |
private struct Ray | |
{ | |
vec3 pos; | |
quat orient; | |
this(vec3 pos, quat orient) | |
{ | |
this.pos = pos; | |
this.orient = orient; | |
} | |
this(float px, float py, float pz, float ow, float ox, float oy, float oz) | |
{ | |
this.pos = vec3(px, py, pz); | |
this.orient = quat(ow, ox, oy, oz); | |
} | |
this(float px, float py, float pz, float ox, float oy, float oz) | |
{ | |
this.pos = vec3(px, py, pz); | |
this.orient = quat(0, ox, oy, oz); | |
} | |
} | |
private struct Joint | |
{ | |
int parentIndex; | |
Ray ray; | |
this(int parentIndex, float px, float py, float pz, float ox, float oy, float oz) | |
{ | |
this.parentIndex = parentIndex; | |
this.ray = Ray(vec3(px, py, pz), quat(0.0, ox, oy, oz)); | |
} | |
this(int parentIndex, vec3 pos, quat orient) | |
{ | |
this.parentIndex = parentIndex; | |
this.ray.pos = pos; | |
this.ray.orient = orient; | |
} | |
} | |
struct Vert | |
{ | |
vec2 uv; | |
uint weightIndex; | |
uint numWeights; | |
} | |
/* This layout should eventually replace Vert I think. Right now I will just copy a mesh's Verts | |
* into a GPUVert[] and send the data to a GL Buffer Object. | |
*/ | |
struct GPUVert | |
{ | |
vec4f[4] weightPos; | |
float[4] weightBiases; | |
vec4f weightIndices; | |
vec2f uv; | |
vec2f pad; | |
} | |
struct Tri | |
{ | |
uint[3] vi; // verts | |
this(int a, int b, int c) | |
{ | |
this.vi[0] = a; | |
this.vi[1] = b; | |
this.vi[2] = c; | |
} | |
} | |
struct Weight | |
{ | |
size_t jointIndex; | |
float weightBias; | |
vec3 pos; | |
this(size_t jointIndex, float weightBias, float posx, float posy, float posz) | |
{ | |
this.jointIndex = jointIndex; | |
this.weightBias = weightBias; | |
this.pos.x = posx; | |
this.pos.y = posy; | |
this.pos.z = posz; | |
} | |
} | |
struct Mesh | |
{ | |
size_t numVerts; | |
Material material; | |
Vert[] verts; | |
size_t numTris; | |
Tri[] tris; | |
size_t numWeights; | |
Weight[] weights; | |
} | |
private enum ParserMode | |
{ | |
open, | |
joints, | |
meshes, | |
bounds, | |
baseframe, | |
frame | |
} | |
/* Changes q.w so that the quaternion is a unit quaternion. | |
* If the other three components do not represent a unit | |
* vector, q.w will be set to 0. | |
*/ | |
void computeUnitQuatW(ref quat q) | |
{ | |
float t = 1f - q.x.sq() - q.y.sq() - q.z.sq(); | |
if (t<=0f) | |
q.w = 0; | |
else | |
q.w = -sqrt(t); | |
} | |
class MD5Model | |
{ | |
Joint[] joints; | |
size_t[string] namedJoints; | |
Mesh[] meshes; | |
float spin; | |
/* Generates a frequency distribution of vertex weight counts */ | |
uint[] getWeightingInfo() | |
{ | |
uint[] rval; | |
foreach (mesh; meshes) | |
{ | |
foreach (vert; mesh.verts) | |
{ | |
auto nw = vert.numWeights; | |
if (nw >= rval.length) | |
rval.length = nw+1; | |
rval[nw]++; | |
} | |
} | |
return rval; | |
} | |
void draw() | |
{ | |
/* THIS DOESN'T EVEN MATTER */ | |
assert(0, "NOT DONE"); | |
foreach (mesh; meshes) | |
{ | |
foreach (tri; mesh.tris) | |
{ | |
foreach (vi; tri.vi) | |
{ | |
Vert vert = mesh.verts[vi]; | |
assert(vert.numWeights == 1, "only one weight per vertex is currently supported"); | |
Weight weight = mesh.weights[vert.weightIndex]; | |
assert(weight.weightBias == 1.0, "weight bias is wrong!"); | |
Joint joint = joints[weight.jointIndex]; | |
vec3 p = joint.ray.pos + weight.pos; | |
vertexer.add(p, | |
vec2(0, 0), /* UVs */ | |
vec3(1, 0, 0), /* normal */ | |
vec3f(.7, .7, .7) /* color */ | |
); | |
} | |
} | |
} | |
} | |
this(string filename) | |
{ | |
spin = 0.0f; | |
int mode = ParserMode.open; | |
size_t nJoints; | |
size_t nMeshes; | |
//string dir = dirName(filename) ~ "/"; | |
foreach (lineNo, line; splitLines(to!string(cast(char[])file.read(filename)))) | |
{ | |
auto words = split(line); | |
if (words.length > 0) | |
switch (mode) | |
{ | |
case ParserMode.open: | |
if (words[0] == "MD5Version") | |
{ | |
assert(to!int(words[1]) == 10); | |
} | |
else if (words[0] == "commandline") | |
{ | |
// do nothing | |
} | |
else if (words[0] == "numJoints") | |
{ | |
nJoints = to!int(words[1]); | |
joints.reserve(nJoints); | |
} | |
else if (words[0] == "numMeshes") | |
{ | |
nMeshes = to!int(words[1]); | |
meshes.reserve(nMeshes); | |
} | |
else if (words[0] == "joints") | |
{ | |
// TODO this parser is bullshit | |
enforce(words[1] == "{"); | |
mode = ParserMode.joints; | |
} | |
else if (words[0] == "mesh") | |
{ | |
enforce(words[1] == "{"); | |
meshes ~= Mesh(); | |
mode = ParserMode.meshes; | |
} | |
break; | |
case ParserMode.joints: | |
if (words[0] == "}") | |
{ | |
enforce(joints.length == nJoints, "wrong number of joints"); | |
mode = ParserMode.open; | |
} | |
else | |
{ | |
enforce(words[0][0] == '"', "joint syntax error 0"); | |
enforce(words[0][$-1] == '"', "joint syntax error 1"); | |
enforce(words[2] == "(", "joint syntax error 2"); | |
enforce(words[6] == ")", "joint syntax error 3"); | |
enforce(words[7] == "(", "joint syntax error 4"); | |
enforce(words[11] == ")", "joint syntax error 5"); | |
quat orient = quat( | |
0f, | |
to!float(words[8]), | |
to!float(words[9]), | |
to!float(words[10])); | |
orient.computeUnitQuatW(); | |
vec3 pos = vec3( | |
to!float(words[3]), | |
to!float(words[4]), | |
to!float(words[5])); | |
Joint joint = Joint(to!int(words[1]), pos, orient); | |
namedJoints[words[0]] = joints.length; // TODO strip quotes | |
joints ~= joint; | |
} | |
break; | |
case ParserMode.meshes: | |
if (words[0] == "}") | |
{ | |
//enforce(meshes.length == nMeshes, "wrong number of meshes"); | |
mode = ParserMode.open; | |
} | |
else if (words[0] == "shader") | |
{ | |
//string textureFilename = dir ~ words[1][1..$-1]; | |
string textureFilename = words[1][1..$-1]; | |
//writefln("[md5] shader \"%s\"", textureFilename); | |
auto materialTexture = new MaterialTexture(); | |
materialTexture.application = TextureApplication.Color; | |
materialTexture.texture = getTexture(textureFilename); | |
auto material = new Material(); | |
material.texes ~= materialTexture; | |
meshes[$-1].material = material; | |
} | |
else if (words[0] == "numverts") | |
{ | |
meshes[$-1].numVerts = to!uint(words[1]); | |
meshes[$-1].verts.reserve(meshes[$-1].numVerts); | |
} | |
else if (words[0] == "vert") | |
{ | |
enforce(to!int(words[1]) == meshes[$-1].verts.length, "mesh vertices out of order"); | |
enforce(words[2] == "(", "vert syntax error 0"); | |
enforce(words[5] == ")", "vert syntax error 1"); | |
Vert vert = Vert( | |
vec2(to!double(words[3]), | |
to!double(words[4])), // uv | |
to!uint(words[6]), // Vert.weightIndex | |
to!uint(words[7])); // Vert.numWeights | |
meshes[$-1].verts ~= vert; | |
} | |
else if (words[0] == "numtris") | |
{ | |
meshes[$-1].numTris = to!size_t(words[1]); | |
meshes[$-1].tris.reserve(meshes[$-1].numTris); | |
} | |
else if (words[0] == "tri") | |
{ | |
enforce(to!size_t(words[1]) == meshes[$-1].tris.length, "mesh tris out of order"); | |
Tri tri = Tri(to!uint(words[2]), | |
to!uint(words[3]), | |
to!uint(words[4])); | |
meshes[$-1].tris ~= tri; | |
} | |
else if (words[0] == "numweights") | |
{ | |
meshes[$-1].numWeights = to!size_t(words[1]); | |
meshes[$-1].weights.reserve(meshes[$-1].numWeights); | |
} | |
else if (words[0] == "weight") | |
{ | |
enforce(to!size_t(words[1]) == meshes[$-1].weights.length, "mesh weights out of order"); | |
enforce(words[4] == "(", "mesh weight syntax error 0"); | |
enforce(words[8] == ")", "mesh weight syntax error 1"); | |
Weight weight = Weight(to!size_t(words[2]), | |
to!float(words[3]), | |
to!float(words[5]), | |
to!float(words[6]), | |
to!float(words[7])); | |
meshes[$-1].weights ~= weight; | |
} | |
break; | |
default: | |
assert(0, "internal error"); | |
} | |
} | |
debug | |
{ | |
//writeln(joints); | |
//writeln(meshes); | |
} | |
auto weightInfo = getWeightingInfo(); | |
if (weightInfo.length > 4) | |
writeln("[warning] some vertices have too many weights: ", filename, ": ", getWeightingInfo()); | |
} | |
} | |
T sq(T)(T v) | |
{ | |
return v*v; | |
} | |
private struct LoadingBone | |
{ | |
int parentIndex; | |
uint componentBits; | |
int firstComponentIndex; | |
} | |
class MD5Animation | |
{ | |
MD5Model model; | |
size_t numFrames; | |
uint frameRate; // frames per second | |
size_t frameStride; // number of joints in animation | |
Joint[] animation; | |
float spin; | |
size_t numJoints; | |
static bool optRenderFull = true; | |
static bool optRenderSoftware; | |
static bool optRenderWireframe; | |
static bool optRenderJoints; | |
static bool optRenderVerts; | |
static bool optRenderWeights; | |
/* t is provided in hecto-nano seconds */ | |
void calculateFrame(ulong t, ref size_t frameNumber0, ref size_t frameNumber1, ref float tween) | |
{ | |
frameNumber0 = (t * cast(ulong)frameRate / 10_000_000) % numFrames; | |
frameNumber1 = (frameNumber0 + 1) % numFrames; | |
tween = t * frameRate % 10_000_000 / 10_000_000f; | |
} | |
// Bone/joint position+orientation for the "base frame." The "base frame" contains all the default | |
// values for each component in the position and orientation of any bone in any frame. Which components | |
// are derived from these default values and which are animated, or derived from frame{} block data, | |
// is specified in the "flags" field, called LoadingBone.componentBits here. | |
Ray[] baseframeBones; | |
// These are the position+orientation for each bone in each frame. This information is derived both | |
// from frame{} blocks and even sometimes the baseframe{} block. See baseframeBones for more. | |
Ray[] frameBones; | |
this(MD5Model model, string filename) | |
{ | |
LoadingBone[] loadingBones; | |
float[] frameAnimatedComponents; | |
size_t numAnimatedComponents; | |
size_t loadingFrameNumber; | |
this.spin = 0.0f; | |
this.model = model; | |
int mode = 0; | |
//auto animationAppender = appender(animation); | |
foreach (lineNo, line; splitLines(to!string(cast(char[])file.read(filename)))) | |
{ | |
auto words = split(line); | |
if (words.length > 0) | |
switch (mode) | |
{ | |
case ParserMode.open: | |
if (words[0] == "MD5Version") | |
{ | |
assert(to!int(words[1]) == 10); | |
} | |
else if (words[0] == "commandline") | |
{ | |
// do nothing | |
} | |
else if (words[0] == "numFrames") | |
{ | |
numFrames = to!size_t(words[1]); | |
} | |
else if (words[0] == "numJoints") | |
{ | |
numJoints = to!size_t(words[1]); | |
enforce(numJoints == model.joints.length, "animation joint count does not equal model joint count"); | |
baseframeBones.reserve(numJoints); | |
} | |
else if (words[0] == "frameRate") | |
{ | |
frameRate = to!uint(words[1]); | |
} | |
else if (words[0] == "numAnimatedComponents") | |
{ | |
numAnimatedComponents = to!size_t(words[1]); | |
// TODO FIXME | |
enforce(numAnimatedComponents % 6 == 0, "numAnimatedComponents: only multiples of 6 supported"); | |
// XXX this seems like a good time to reserve | |
// some memory though it may not be ideal | |
// for all MD5 files | |
loadingBones.reserve(numJoints); | |
frameAnimatedComponents.reserve(numJoints * numAnimatedComponents); | |
} | |
else if (words[0] == "hierarchy") | |
{ | |
enforce(words[1] == "{"); | |
mode = ParserMode.joints; | |
} | |
else if (words[0] == "bounds") | |
{ | |
enforce(words[1] == "{"); | |
mode = ParserMode.bounds; | |
} | |
else if (words[0] == "baseframe") | |
{ | |
enforce(words[1] == "{"); | |
mode = ParserMode.baseframe; | |
} | |
else if (words[0] == "frame") | |
{ | |
//writefln("animation.length: %d, frame # %d", animation.length, to!size_t(words[1])); | |
//enforce(to!size_t(words[1]) == animation.length, "frames out of order"); | |
enforce(words[2] == "{"); | |
loadingFrameNumber = to!int(words[1]); | |
frameAnimatedComponents.length = 0; | |
mode = ParserMode.frame; | |
} | |
break; | |
case ParserMode.joints: | |
if (words[0] == "}") | |
{ | |
enforce(loadingBones.length == numJoints, "numJoints and hierarchy mismatch"); | |
mode = ParserMode.open; | |
} | |
else | |
{ | |
loadingBones ~= LoadingBone( | |
to!int(words[1]), | |
to!uint(words[2]), | |
to!int(words[3])); | |
} | |
break; | |
case ParserMode.bounds: | |
if (words[0] == "}") | |
{ | |
// TODO sanity check | |
mode = ParserMode.open; | |
} | |
break; | |
case ParserMode.baseframe: | |
if (words[0] == "}") | |
{ | |
// TODO sanity check | |
mode = ParserMode.open; | |
} | |
else | |
{ | |
enforce(words[0] == "(", "baseframe syntax error 0"); | |
enforce(words[4] == ")", "baseframe syntax error 1"); | |
enforce(words[5] == "(", "baseframe syntax error 2"); | |
enforce(words[9] == ")", "baseframe syntax error 3"); | |
baseframeBones ~= Ray( | |
to!float(words[1]), | |
to!float(words[2]), | |
to!float(words[3]), | |
to!float(words[6]), | |
to!float(words[7]), | |
to!float(words[8])); | |
} | |
break; | |
case ParserMode.frame: | |
if (words[0] == "}") | |
{ | |
enforce(frameAnimatedComponents.length == numAnimatedComponents, | |
"frame{} block has wrong number of elements"); | |
foreach (boneIndex, loadingBone; loadingBones) | |
{ | |
size_t animatedComponentIndex = 0; | |
//enforce(animatedComponentIndex == loadingBone.firstComponentIndex, | |
// "got bad animated component ordering data from hierarchy{} block"); | |
Ray bone = baseframeBones[boneIndex]; | |
if (loadingBone.componentBits & 1) | |
bone.pos.x = frameAnimatedComponents[loadingBone.firstComponentIndex + animatedComponentIndex++]; | |
if (loadingBone.componentBits & 2) | |
bone.pos.y = frameAnimatedComponents[loadingBone.firstComponentIndex + animatedComponentIndex++]; | |
if (loadingBone.componentBits & 4) | |
bone.pos.x = frameAnimatedComponents[loadingBone.firstComponentIndex + animatedComponentIndex++]; | |
if (loadingBone.componentBits & 8) | |
bone.orient.x = frameAnimatedComponents[loadingBone.firstComponentIndex + animatedComponentIndex++]; | |
if (loadingBone.componentBits & 16) | |
bone.orient.y = frameAnimatedComponents[loadingBone.firstComponentIndex + animatedComponentIndex++]; | |
if (loadingBone.componentBits & 32) | |
bone.orient.z = frameAnimatedComponents[loadingBone.firstComponentIndex + animatedComponentIndex++]; | |
// Normalize orientation quaternion | |
computeUnitQuatW(bone.orient); | |
// Reposition and reorient bone relative to its parent, unless it is the root bone | |
if (loadingBone.parentIndex >= 0) | |
{ | |
Ray parentBone = frameBones[loadingFrameNumber*numJoints + loadingBone.parentIndex]; | |
bone.pos = parentBone.pos + (parentBone.orient * bone.pos); | |
bone.orient = parentBone.orient * bone.orient; | |
bone.orient.normalize(); | |
} | |
// Add bone to the animation | |
frameBones ~= bone; | |
} | |
mode = ParserMode.open; | |
} | |
else | |
{ | |
//writeln("animationAppender.put()"); | |
//animationAppender.put(map!(to!float)(words)); | |
// TODO FIXME | |
enforce(words.length == 6, "only 6 components per frame{} block lines supported"); | |
frameAnimatedComponents ~= to!float(words[0]); | |
frameAnimatedComponents ~= to!float(words[1]); | |
frameAnimatedComponents ~= to!float(words[2]); | |
frameAnimatedComponents ~= to!float(words[3]); | |
frameAnimatedComponents ~= to!float(words[4]); | |
frameAnimatedComponents ~= to!float(words[5]); | |
} | |
break; | |
default: | |
writefln("internal error: unknown parse mode: %d", mode); | |
assert(0, "internal error"); | |
} | |
} | |
debug | |
{ | |
//writeln(animation); | |
} | |
} | |
void renderSkeleton(mat4 mvmat, mat4 pmat) | |
{ | |
size_t frameNumber, frameNumber1; | |
float tween; | |
// Draw joint positions | |
foreach (bone; interpolatedSkeleton) | |
{ | |
vertexer.add(bone.pos, vec2(0,0), vec3(1,0,0), vec3f(1,0,0)); | |
} | |
vertexer.draw(shaderProgram, mvmat, pmat, emptyMaterial, GL_POINTS); | |
// Draw bones | |
foreach(boneIndex, bone; interpolatedSkeleton) | |
{ | |
auto parentIndex = model.joints[boneIndex].parentIndex; | |
if (parentIndex != -1) | |
{ | |
vertexer.add(bone.pos, vec2(0,0), vec3(1,0,0), vec3f(0,1,0)); | |
Ray parentBone = interpolatedSkeleton[parentIndex]; | |
vertexer.add(parentBone.pos, vec2(0,0), vec3(1,0,0), vec3f(0,1,0)); | |
} | |
} | |
vertexer.draw(shaderProgram, mvmat, pmat, emptyMaterial, GL_LINES); | |
//writefln("frame # %d/%d", frameNumber, numFrames); | |
spin += 0.5; | |
} | |
// render() | |
static vec3[] vertPosBuf; | |
static vec3[] vertNorBuf; | |
void render(mat4 mvmat, mat4 pmat, vec4f color=vec4f(1,1,1,1)) | |
{ | |
vec3f color3 = vec3f(color.rgb); | |
size_t frameNumber, frameNumber1; | |
float tween; | |
//vec3[] vertsNormals; | |
foreach (mesh; model.meshes) | |
{ | |
if (vertPosBuf.length < mesh.verts.length) | |
{ | |
vertPosBuf.length = mesh.verts.length; | |
vertNorBuf.length = mesh.verts.length; | |
} | |
/* Calculate mesh vertex positions from animation weight positions */ | |
foreach (vi; 0..mesh.verts.length) | |
{ | |
Vert vert = mesh.verts[vi]; | |
Weight[] weights = mesh.weights[vert.weightIndex .. vert.weightIndex + vert.numWeights]; | |
vec3 pos = vec3(0,0,0); | |
foreach (weight; weights) | |
{ | |
auto joint = interpolatedSkeleton[weight.jointIndex]; | |
pos += (joint.orient * weight.pos + joint.pos) * weight.weightBias; | |
} | |
vertPosBuf[vi] = pos; | |
vertNorBuf[vi] = vec3(0,0,0); | |
} | |
/* Calculate and accumulate triangle normals */ | |
foreach (ti, tri; mesh.tris) | |
{ | |
auto vi0 = tri.vi[0], | |
vi1 = tri.vi[1], | |
vi2 = tri.vi[2]; | |
auto v0 = vertPosBuf[vi0], | |
v1 = vertPosBuf[vi1], | |
v2 = vertPosBuf[vi2]; | |
/* Calculate triangle's normal */ | |
auto normal = cross(v2-v0, v1-v0); | |
vertNorBuf[vi0] += normal; | |
vertNorBuf[vi1] += normal; | |
vertNorBuf[vi2] += normal; | |
} | |
/* Send all vertex data to vertexer */ | |
/* TODO either integrate with vertexer more intimately, or send the vertex data to the | |
* GL by hand here! | |
*/ | |
foreach (tri; mesh.tris) | |
{ | |
foreach (vi; tri.vi) | |
{ | |
vertexer.add( | |
vertPosBuf[vi], | |
mesh.verts[vi].uv, | |
vertNorBuf[vi].normalized, | |
color3); | |
} | |
} | |
/* Draw vertexer contents */ | |
vertexer.draw(shaderProgram1, mvmat, pmat, mesh.material, GL_TRIANGLES); | |
} | |
} | |
void renderWeights(mat4 mvmat, mat4 pmat) | |
{ | |
size_t frameNumber, frameNumber1; | |
float tween; | |
foreach (mesh; model.meshes) | |
{ | |
/* Calculate mesh vertex positions from animation weight positions */ | |
foreach (vi; 0..mesh.verts.length) | |
{ | |
Vert vert = mesh.verts[vi]; | |
Weight[] weights = mesh.weights[vert.weightIndex .. vert.weightIndex + vert.numWeights]; | |
vec3 pos = vec3(0,0,0); | |
foreach (weight; weights) | |
{ | |
auto joint = interpolatedSkeleton[weight.jointIndex]; | |
auto weightPos = joint.orient * weight.pos + joint.pos; | |
vertexer.add(weightPos, vec2(0,0), vec3(1,0,0), vec3f(1,1,1)); | |
} | |
} | |
/* Draw vertexer contents */ | |
vertexer.draw(varyingColorShaderProgram, mvmat, pmat, null, GL_POINTS); | |
} | |
} | |
// renderGPU() | |
mat4f[] boneMatrices; | |
bool gpuInitialized; | |
GLint mvmatUniloc; | |
GLint pmatUniloc; | |
GLint boneMatricesUniloc; | |
GLint colorMapUniloc; | |
GLint colorUniloc; | |
GLint uvAttloc; | |
GLint boneIndicesAttloc; | |
GLint weightBiasesAttloc; | |
GLint weightPosAttloc; | |
GLint indBuf; | |
/* GL Buffer Objects to hold vertex attributes and face indices. One per mesh. */ | |
GLuint[] vbo; | |
GLuint[] ibo; | |
void renderGPU(mat4 mvmat, mat4 pmat, vec4f color=vec4f(1,1,1,1)) | |
{ | |
initGPU(); | |
/* Create our array of bone matrices describing the armature/skeleton */ | |
/* Resize if necessary the array we reuse for storing bone matrices */ | |
if (boneMatrices.length < numJoints) | |
boneMatrices.length = numJoints; | |
/* Calculate the value of each bone matrix */ | |
foreach (iBone, bone; interpolatedSkeleton[0..numJoints]) | |
{ | |
vec3f[3] v0 = [ | |
vec3f(bone.orient * vec3(1,0,0)), | |
vec3f(bone.orient * vec3(0,1,0)), | |
vec3f(bone.orient * vec3(0,0,1)), | |
]; | |
/* Create a rotation matrix representing the orientation of this joint/bone */ | |
mat4f m0 = mat4f( //bone.orient.to_matrix!(4,4); | |
v0[0].x, v0[0].y, v0[0].z, bone.pos.x, | |
v0[1].x, v0[1].y, v0[1].z, bone.pos.y, | |
v0[2].x, v0[2].y, v0[2].z, bone.pos.z, | |
0f, 0f, 0f, 1f); | |
mat4f m1 = bone.orient.to_matrix!(4,4); | |
//writeln("quat.to_matrix: ", m1); | |
//writeln("quat.meeeeeeee: ", m0); | |
/* Factor in translation of this joint/bone */ | |
mat4f boneMatrix = m1.translate(bone.pos.x, bone.pos.y, bone.pos.z); | |
/* Assign the bone matrix to the array */ | |
boneMatrices[iBone] = boneMatrix; | |
//writefln("bone %d matrix: %s", iBone, boneMatrix.as_pretty_string); | |
} | |
//writeln("BONE MATRICES ******** ", boneMatrices); | |
//writeln("mvmat ******** ", mvmat); | |
/* Select our shader program */ | |
md5ShaderProgram.use(); | |
/* Send our uniforms to the GL shader program */ | |
/* Send our bone matrices */ | |
glUniformMatrix4fv(boneMatricesUniloc, cast(GLint)numJoints, GL_TRUE, cast(float*)boneMatrices.ptr); | |
glErrorCheck("sent bone matrices"); | |
/* TODO stop using doubles EVERYWHERE wtf is wrong with you */ | |
mat4f tempMatrix; | |
/* Send model-view matrix TODO merge MVP! */ | |
tempMatrix = mat4f(mvmat); | |
glUniformMatrix4fv(mvmatUniloc, 1, GL_TRUE, tempMatrix.value_ptr); | |
glErrorCheck("sent mvmat uniform"); | |
/* Send projection matrix */ | |
tempMatrix = mat4f(pmat); | |
glUniformMatrix4fv(pmatUniloc, 1, GL_TRUE, tempMatrix.value_ptr); | |
glErrorCheck("sent pmat uniform"); | |
/* Send draw command for each mesh! */ | |
foreach (iMesh, mesh; model.meshes) | |
{ | |
/* Select our GL buffer object containing our vertex data */ | |
glBindBuffer(GL_ARRAY_BUFFER, vbo[iMesh]); | |
glErrorCheck("md5 1"); | |
/* Enable our vertex attributes */ | |
static if (0) writefln(` | |
attribute location: boneIndices: %d | |
attribute location: weightBiases: %d | |
attribute location: weightPos: %d | |
attribute location: uv: %d`, | |
boneIndicesAttloc, | |
weightBiasesAttloc, | |
weightPosAttloc, | |
uvAttloc); | |
glEnableVertexAttribArray(uvAttloc); | |
glEnableVertexAttribArray(boneIndicesAttloc+0); | |
glEnableVertexAttribArray(weightBiasesAttloc); | |
glEnableVertexAttribArray(weightPosAttloc+0); | |
glEnableVertexAttribArray(weightPosAttloc+1); | |
glEnableVertexAttribArray(weightPosAttloc+2); | |
glEnableVertexAttribArray(weightPosAttloc+3); | |
/* Specify our vertex attribute layout (actual data in VBO already) */ | |
foreach (i; 0..4) | |
glVertexAttribPointer(weightPosAttloc+i, 4, GL_FLOAT, GL_FALSE, GPUVert.sizeof, cast(void*)(4*4*i)); | |
glVertexAttribPointer(weightBiasesAttloc, 4, GL_FLOAT, GL_FALSE, GPUVert.sizeof, cast(void*)64); | |
glVertexAttribPointer(boneIndicesAttloc, 4, GL_FLOAT, GL_FALSE, GPUVert.sizeof, cast(void*)(80)); | |
glVertexAttribPointer(uvAttloc, 2, GL_FLOAT, GL_FALSE, GPUVert.sizeof, cast(void*)96); | |
/* Set texture sampler for color map TODO use Material better! */ | |
glActiveTexture(GL_TEXTURE0); | |
glBindTexture(GL_TEXTURE_2D, mesh.material.texes[0].texture); | |
glUniform1i(colorMapUniloc, 0); | |
glErrorCheck("md5 9.1"); | |
glUniform4fv(colorUniloc, 1, color.value_ptr); | |
glErrorCheck("md5 9.1.1"); | |
/* Draw! */ | |
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo[iMesh]); | |
glErrorCheck("md5 9"); | |
glDrawElements(GL_TRIANGLES, cast(int)(mesh.numTris*3), GL_UNSIGNED_INT, cast(void*)0); | |
glErrorCheck("md5 10"); | |
} | |
/* Release XXX */ | |
/* Release vert attributes */ | |
glDisableVertexAttribArray(uvAttloc); | |
glDisableVertexAttribArray(boneIndicesAttloc); | |
glDisableVertexAttribArray(weightBiasesAttloc); | |
glDisableVertexAttribArray(weightPosAttloc+0); | |
glDisableVertexAttribArray(weightPosAttloc+1); | |
glDisableVertexAttribArray(weightPosAttloc+2); | |
glDisableVertexAttribArray(weightPosAttloc+3); | |
/* Disable the buffer objects we've used */ | |
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); | |
glBindBuffer(GL_ARRAY_BUFFER, 0); | |
/* Unset texture samplers TODO use Material better! */ | |
glActiveTexture(GL_TEXTURE0); | |
glBindTexture(GL_TEXTURE_2D, 0); | |
glErrorCheck("renderGPU finished"); | |
} | |
bool initGPUDone; | |
void initGPU() | |
{ | |
if (initGPUDone) | |
return; | |
/* Generate needed buffers */ | |
/* Vertex buffer objects for each mesh */ | |
vbo.length = model.meshes.length; | |
/* Index buffer objects for face indices */ | |
ibo.length = vbo.length; | |
/* Create buffer objects for both vertex data and face data */ | |
glGenBuffers(cast(GLint)vbo.length, vbo.ptr); | |
glGenBuffers(cast(GLint)ibo.length, ibo.ptr); | |
glErrorCheck("glGenBuffers()"); | |
GPUVert[] data; | |
foreach (iMesh, mesh; model.meshes) | |
{ | |
if (data.length < mesh.verts.length) | |
data.length = mesh.verts.length; | |
foreach (iVert, vert; mesh.verts) | |
{ | |
GPUVert v; | |
v.uv = vec2f(vert.uv.x, vert.uv.y); | |
foreach (iWeight; 0..4) | |
{ | |
if (iWeight < vert.numWeights) | |
{ | |
auto weight = mesh.weights[vert.weightIndex + iWeight]; | |
//writefln("vert %d weight %d joint %d", iVert, iWeight, weight.jointIndex); | |
v.weightIndices.vector[iWeight] = cast(uint)weight.jointIndex; | |
v.weightBiases [iWeight] = cast(float)weight.weightBias; | |
v.weightPos [iWeight] = vec4f(weight.pos.x, weight.pos.y, weight.pos.z, 1f); | |
} | |
else | |
{ | |
v.weightIndices.vector[iWeight] = 0f; | |
v.weightBiases [iWeight] = 0f; | |
v.weightPos [iWeight] = vec4f(0,0,0,0); | |
} | |
v.pad = vec2f(666f, 666f); | |
} | |
if (vert.numWeights == 2 && | |
mesh.weights[vert.weightIndex].jointIndex < mesh.weights[vert.weightIndex+1].jointIndex) | |
v.pad.x = 1f; | |
data[iVert] = v; | |
} | |
/* Send vertex attributes to its GL Buffer Object */ | |
/* Create the buffer object in the GL */ | |
glBindBuffer(GL_ARRAY_BUFFER, vbo[iMesh]); | |
/* Fill the buffer object with our data */ | |
//writeln("vbo data: "); | |
version (debugMD5) | |
{ | |
foreach(v; data) if (v.pad.x == 1f) writefln(` | |
weight 0 pos: %s | |
weight 1 pos: %s | |
weight 2 pos: %s | |
weight 3 pos: %s | |
weight biases: %s | |
weight indices: %s | |
uv: %s | |
pad: %s`, | |
v.weightPos[0], | |
v.weightPos[1], | |
v.weightPos[2], | |
v.weightPos[3], | |
v.weightBiases, | |
v.weightIndices, | |
v.uv, v.pad); | |
GLint maxVA; | |
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &maxVA); | |
writefln("%d %d-byte triangles %d bytes tota (%d max!): %s", | |
mesh.tris.length, Tri.sizeof, Tri.sizeof * mesh.tris.length, maxVA, mesh.tris); | |
} | |
glBufferData(GL_ARRAY_BUFFER, GPUVert.sizeof * data.length, data.ptr, GL_STATIC_DRAW); | |
/* Finish using this buffer object */ | |
glBindBuffer(GL_ARRAY_BUFFER, 0); | |
/* Send face index data to its GL Buffer Object */ | |
/* Create the buffer object in the GL */ | |
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo[iMesh]); | |
/* Fill the buffer object with our data */ | |
glBufferData(GL_ELEMENT_ARRAY_BUFFER, Tri.sizeof * mesh.tris.length, mesh.tris.ptr, GL_STATIC_DRAW); | |
/* Finish using this buffer object */ | |
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); | |
glErrorCheck("md5 end of initGPU()"); | |
} | |
/* Grab shader variable locations */ | |
mvmatUniloc = md5ShaderProgram.getUniformLocation("viewMatrix"); | |
pmatUniloc = md5ShaderProgram.getUniformLocation("projMatrix"); | |
boneMatricesUniloc = md5ShaderProgram.getUniformLocation("boneMatrices"); | |
colorMapUniloc = md5ShaderProgram.getUniformLocation("colorMap"); | |
colorUniloc = md5ShaderProgram.getUniformLocation("colorU"); | |
uvAttloc = md5ShaderProgram.getAttribLocation ("uvV"); | |
boneIndicesAttloc = md5ShaderProgram.getAttribLocation ("boneIndices"); | |
weightBiasesAttloc = md5ShaderProgram.getAttribLocation ("weightBiases"); | |
weightPosAttloc = md5ShaderProgram.getAttribLocation ("weightPos[0]"); | |
glErrorCheck("initGPU finished"); | |
initGPUDone = true; | |
} | |
void renderVerts(mat4 mvmat, mat4 pmat) | |
{ | |
size_t frameNumber, frameNumber1; | |
float tween; | |
foreach (mesh; model.meshes) | |
{ | |
foreach (tri; mesh.tris) | |
{ | |
vec3[3] outVerts; | |
foreach (outVertI, vi; tri.vi) | |
{ | |
outVerts[outVertI] = vec3(0, 0, 0); | |
Vert vert = mesh.verts[vi]; | |
Weight[] weights = mesh.weights[vert.weightIndex .. vert.weightIndex + vert.numWeights]; | |
foreach (weight; weights) | |
{ | |
auto joint = interpolatedSkeleton[weight.jointIndex]; | |
outVerts[outVertI] += (joint.orient * weight.pos + joint.pos) * weight.weightBias; | |
} | |
} | |
vertexer.add(outVerts[0], vec2(0,0), vec3(0,0,0), vec3f(.2,.2,1)); | |
vertexer.add(outVerts[1], vec2(1,1), vec3(1,1,1), vec3f(.2,.2,1)); | |
vertexer.add(outVerts[2], vec2(2,2), vec3(2,2,2), vec3f(.2,.2,1)); | |
vertexer.add(outVerts[0], vec2(0,0), vec3(0,0,0), vec3f(.2,.2,1)); | |
} | |
vertexer.draw(shaderProgram, mvmat, pmat, mesh.material, GL_LINES); | |
} | |
} | |
void draw(mat4 mvmat, mat4 pmat, ulong t, vec4f color=vec4f(1,1,1,1)) | |
{ | |
if (vertexer is null) | |
{ | |
vertexer = new Vertexer(); | |
emptyMaterial = new Material(); | |
shaderProgram = new ShaderProgram("simple-red.vs", "simple-red.fs"); | |
shaderProgram1 = new ShaderProgram("simpler.vs", "simpler.fs"); | |
md5ShaderProgram = new ShaderProgram("md5-color--uv--uv-color.vs", "simpler.fs"); | |
varyingColorShaderProgram = new ShaderProgram("simpler.vs", "simple-color.fs"); | |
} | |
calculateInterpolatedSkeleton(t); | |
if (optRenderFull) | |
{ | |
glEnable(GL_CULL_FACE); | |
if (optRenderSoftware) | |
render(mvmat, pmat, color); | |
else | |
renderGPU(mvmat, pmat, color); | |
} | |
if (optRenderWeights) | |
{ | |
glDisable(GL_DEPTH_TEST); | |
glPointSize(5f); | |
renderWeights(mvmat, pmat); | |
glPointSize(1f); | |
glEnable(GL_DEPTH_TEST); | |
} | |
if (optRenderWireframe) | |
renderSkeleton(mvmat, pmat); | |
if (optRenderVerts) | |
renderVerts(mvmat, pmat); | |
} | |
/* We can store an interpolated skeleton (a slice of frameBones) here, allowing us to | |
* avoid recalculating a given skeleton, and also providing a place in memory to store | |
* it, sans alloca. | |
*/ | |
static Ray[] interpolatedSkeleton; | |
void calculateInterpolatedSkeleton(ulong t) | |
{ | |
size_t f0, f1; | |
float f01; | |
calculateFrame(t, f0, f1, f01); | |
if (interpolatedSkeleton.length < numJoints) | |
interpolatedSkeleton.length = numJoints; | |
foreach (iBone; 0..numJoints) | |
{ | |
auto b0 = frameBones[f0 * numJoints + iBone]; | |
auto b1 = frameBones[f1 * numJoints + iBone]; | |
interpolatedSkeleton[iBone].pos = lerp(b0.pos, b1.pos, f01); | |
interpolatedSkeleton[iBone].orient = lerp(b0.orient, b1.orient, f01); // TODO use slerp! | |
} | |
} | |
} | |
class MD5Animator | |
{ | |
MD5Animation anim; | |
ulong start; // hnsecs! | |
/* TODO allowing 'now' to have a default value is only useful in the world of everything | |
* just being a single stupid looping animation. i should refactor a bit and make this | |
* better. | |
*/ | |
this(MD5Animation anim) | |
{ | |
this.anim = anim; | |
this.start = GameTime.gt; | |
} | |
/* now = current time | |
*/ | |
void draw(mat4 mvmat, mat4 pmat, vec4f color=vec4f(1,1,1,1)) | |
{ | |
/* TODO animation sequences instead of just looping the same animation */ | |
anim.draw(mvmat, pmat, GameTime.gt-start, color); | |
} | |
} | |
void stop() { | |
writeln("STOP"); | |
} | |
void glErrorCheck(string source) | |
{ | |
GLenum err = glGetError(); | |
if (err) | |
{ | |
writefln("error @ %s: opengl: %s", source, err); | |
stop(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment