Skip to content

Instantly share code, notes, and snippets.

@hdon
Created January 3, 2015 13:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hdon/69045a8b3cd29d1bbfe2 to your computer and use it in GitHub Desktop.
Save hdon/69045a8b3cd29d1bbfe2 to your computer and use it in GitHub Desktop.
md5.d
#version 150
/* MD5 fragment shader */
#define AMB 0.2
uniform sampler2D colorMap;
uniform float ambU;
in vec2 uvF;
in vec3 normalF;
out vec4 outputF;
void main()
{
outputF = texture(colorMap, uvF) * (smoothstep(-1, 1, dot(normalize(normalF), vec3(0,1,0))) * (1.0-ambU) + ambU);
}
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;
import gl3n.interpolate : lerp;
import ants.vertexer;
import ants.material;
import ants.shader;
import ants.texture;
import ants.gametime;
import ants.glutil;
import main = ants.main;
import lines = ants.lines;
import std.math : sqrt;
import std.stdio : writeln, writefln;
alias sum = reduce!"a + b";
private
{
Vertexer vertexer;
Material emptyMaterial;
ShaderProgram shaderProgram;
ShaderProgram shaderProgram1;
ShaderProgram md5ShaderProgram;
ShaderProgram shaderProgram2;
/* Attribute locations */
GLint sp2_Aposition;
GLint sp2_Anormal;
GLint sp2_Auv;
GLint sp2_UviewMatrix;
GLint sp2_UprojMatrix;
GLint sp2_UnormalMatrix;
GLint sp2_UcolorMap;
GLint sp2_Uamb;
}
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;
vec3 normal;
uint weightIndex;
uint numWeights;
this(vec2 uv, uint weightIndex, uint numWeights)
{
this.uv = uv;
this.weightIndex = weightIndex;
this.numWeights = 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
{
vec4[4] weightPos;
float[4] weightBiases;
float[4] boneIndices;
vec2 uv;
vec3 normal;
}
struct Tri
{
uint[3] vi; // verts
this(uint a, uint b, uint 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;
}
}
class Mesh
{
size_t numVerts;
Material material;
Vert[] verts;
size_t numTris;
Tri[] tris;
size_t numWeights;
Weight[] weights;
this() {}
/* Renderers of mesh may consolidate all mesh data into a single vertex buffer.
* This may help with that.
*/
size_t firstVertexIndex;
size_t firstElementIndex;
}
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;
/* Generate bone-space weight normals */
static struct BindPoseVert
{
vec3 pos;
vec3 normal;
vec2 uv;
}
static BindPoseVert[] bindPoseVerts;
void generateBindPose(size_t iMesh, bool inBoneSpace=false)
{
auto mesh = meshes[iMesh];
if (bindPoseVerts.length < mesh.verts.length)
bindPoseVerts.length = mesh.verts.length;
foreach (iVert, vert; mesh.verts)
{
Weight[] weights = mesh.weights[vert.weightIndex .. vert.weightIndex + vert.numWeights];
auto pos = vec3(0, 0, 0);
foreach (weight; weights)
{
auto joint = joints[weight.jointIndex].ray;
pos += weight.weightBias *
//(joint.orient * weight.pos + joint.pos) * weight.weightBias
//weight.pos * weight.weightBias
//(joint.pos + joint.orient * weight.pos)
(joint.orient * weight.pos + joint.pos)
//weight.pos
;
}
bindPoseVerts[iVert] = BindPoseVert(pos, vec3(0,0,0), vert.uv);
writefln("mesh %d vert %d = %s", iMesh, iVert, pos);
}
foreach (iTri, tri; mesh.tris)
{
auto a = bindPoseVerts[tri.vi[0]].pos;
auto b = bindPoseVerts[tri.vi[1]].pos;
auto c = bindPoseVerts[tri.vi[2]].pos;
auto normal = cross((b-a).normalized, (b-c).normalized).normalized;
bindPoseVerts[tri.vi[0]].normal += normal;
bindPoseVerts[tri.vi[1]].normal += normal;
bindPoseVerts[tri.vi[2]].normal += normal;
}
foreach (iVert; 0..mesh.verts.length)
{
auto meshVert = mesh.verts[iVert];
auto n = bindPoseVerts[iVert].normal.normalized;
if (inBoneSpace)
{
auto n2 = vec3(0,0,0);
foreach (iWeight, weight; mesh.weights[meshVert.weightIndex .. meshVert.weightIndex + meshVert.numWeights])
n2 += joints[weight.jointIndex].ray.orient.inverse * n * weight.weightBias;
bindPoseVerts[iVert].normal = n2;
}
else
bindPoseVerts[iVert].normal = n;
}
}
/* Vertex Array Object, and Buffer Objects */
GLuint vao, vbo, ibo;
static Tri[] triBuf;
void render(mat4 viewMatrix, mat4 projMatrix)
{
if (shaderProgram2 is null)
{
shaderProgram2 = new ShaderProgram("vert-uv-norm--uv-norm.glsl", "frag-colorMap--uv-normal--uv-normal.glsl");
glGenBuffers(2, &vbo);
glGenVertexArrays(1, &vao);
shaderProgram2.use;
glBindVertexArray(vao);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
auto numTris = sum(map!"a.tris.length"(meshes));
writeln("numTris=", numTris);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, numTris * Tri.sizeof, null, GL_STREAM_DRAW);
auto numVerts = sum(map!"a.verts.length"(meshes));
glBufferData(GL_ARRAY_BUFFER, numVerts * BindPoseVert.sizeof, null, GL_STREAM_DRAW);
size_t vboWriteCursor, iboWriteCursor;
int vertexCounter, triCounter;
auto maxTris = reduce!"a>b?a:b"(map!"a.tris.length"(meshes));
if (triBuf.length < maxTris)
triBuf.length = maxTris;
foreach (iMesh, mesh; meshes)
{
mesh.firstVertexIndex = vertexCounter;
mesh.firstElementIndex = triCounter * 3;
writefln("mesh[%d].firstElementIndex = %d", iMesh, mesh.firstElementIndex);
generateBindPose(iMesh);
auto vboWriteLen = mesh.verts.length * BindPoseVert.sizeof;
glBufferSubData(GL_ARRAY_BUFFER, vboWriteCursor, vboWriteLen, bindPoseVerts.ptr);
writefln("mesh %d writing %d vertices starting at index %d (%d bytes): %s",
iMesh, mesh.verts.length, vertexCounter, vboWriteCursor, bindPoseVerts[0..mesh.verts.length]);
foreach (iTri, tri; mesh.tris)
{
triBuf[iTri] = Tri(
vertexCounter + tri.vi[0]
, vertexCounter + tri.vi[1]
, vertexCounter + tri.vi[2]
);
}
auto iboWriteLen = mesh.tris.length * triBuf[0].sizeof;
glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, iboWriteCursor, iboWriteLen, triBuf.ptr);
writefln("mesh %d writing %d triangles / %d elements / %d bytes starting at tri index %d / element index %d (%d bytes): %s",
iMesh, mesh.tris.length, mesh.tris.length * 3, iboWriteLen, triCounter, triCounter * 3, iboWriteCursor,
triBuf[0..mesh.tris.length]);
vboWriteCursor += vboWriteLen;
iboWriteCursor += iboWriteLen;
vertexCounter += mesh.verts.length;
triCounter += mesh.tris.length;
glFinish();
}
assert(triCounter == numTris);
assert(vertexCounter == numVerts);
sp2_UviewMatrix = shaderProgram2.getUniformLocation("viewMatrix");
sp2_UprojMatrix = shaderProgram2.getUniformLocation("projMatrix");
sp2_UnormalMatrix = shaderProgram2.getUniformLocation("normalMatrix");
sp2_UcolorMap = shaderProgram2.getUniformLocation("colorMap");
sp2_Uamb = shaderProgram2.getUniformLocation("ambU");
sp2_Aposition = shaderProgram2.getAttribLocation ("positionV");
sp2_Anormal = shaderProgram2.getAttribLocation ("normalV");
sp2_Auv = shaderProgram2.getAttribLocation ("uvV");
assert(sp2_UviewMatrix >= 0);
assert(sp2_UprojMatrix >= 0);
assert(sp2_Aposition >= 0);
BindPoseVert VT;
glEnableVertexAttribArray(sp2_Aposition);
glVertexAttribPointer(sp2_Aposition, 3, GL_FLOAT, GL_FALSE,
VT.sizeof, cast(void*) VT.pos.offsetof);
if (sp2_Anormal >= 0)
{
glEnableVertexAttribArray(sp2_Anormal);
glVertexAttribPointer(sp2_Anormal, 3, GL_FLOAT, GL_FALSE,
VT.sizeof, cast(void*) VT.normal.offsetof);
}
if (sp2_Auv >= 0)
{
glEnableVertexAttribArray(sp2_Auv);
glVertexAttribPointer(sp2_Auv, 3, GL_FLOAT, GL_FALSE,
VT.sizeof, cast(void*) VT.uv.offsetof);
}
}
else
{
shaderProgram2.use;
glBindVertexArray(vao);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
}
glUniformMatrix4fv(sp2_UviewMatrix, 1, GL_TRUE, viewMatrix.value_ptr);
glUniformMatrix4fv(sp2_UprojMatrix, 1, GL_TRUE, projMatrix.value_ptr);
static if (0)
if (sp2_UnormalMatrix >= 0)
{
mat3 normalMatrix = viewMatrix.get_rotation;
glUniformMatrix3fv(sp2_UnormalMatrix, 1, GL_TRUE, normalMatrix.value_ptr);
}
//auto buf = new float[1536/float.sizeof];
//glGetBufferSubData(GL_ARRAY_BUFFER, 0, buf.length * buf[0].sizeof, buf.ptr);
//writeln("got: ", buf);
foreach (iMesh, mesh; meshes)
{
if (sp2_UcolorMap >= 0)
{
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, mesh.material.texes[0].texture);
glUniform1i(sp2_UcolorMap, 0);
}
if (sp2_Uamb >= 0)
glUniform1f(sp2_Uamb, mesh.material.amb);
GLint start = cast(GLint) mesh.firstElementIndex;
GLint count = cast(GLint) mesh.tris.length * 3;
GLint end = start + count;
//writefln("mesh[%d].firstElementIndex == %d", iMesh, mesh.firstElementIndex);
//writefln("meshes[%d] glDrawRangeElements(GL_TRIANGLES, %s, %s, %s, GL_UNSIGNED_INT, null)", iMesh, start, end, count);
glDrawRangeElements(GL_TRIANGLES, start, end, count, GL_UNSIGNED_INT, cast(void*) (start * uint.sizeof));
}
if (sp2_UcolorMap >= 0)
glBindTexture(GL_TEXTURE_2D, 0);
glBindVertexArray(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
/* 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;
}
this(string filename)
{
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 ~= new 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);
if (joint.parentIndex >= 0)
{
Ray parentBone = joints[joint.parentIndex].ray;
// I used to do this, because you do it for md5anim, but looking at io_scene_md5.py
// MD5 exporter for blender, it looks like the joints{} block in the .md5mesh, providing
// the "bind pose," each bone is already expressed in object space, so there is no reason
// to compose it with the transform of its parent, down to the root bone.
// joint.ray.pos = parentBone.pos + (parentBone.orient * joint.ray.pos);
// joint.ray.orient = parentBone.orient * joint.ray.orient;
// joint.ray.orient.normalize();
}
namedJoints[words[0][1..$-1]] = joints.length;
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];
auto materialTexture = new MaterialTexture();
materialTexture.application = TextureApplication.Color;
auto material = new Material();
if (textureFilename == "Framebuffer")
{
materialTexture.texture = main.marioTex;
material.amb = 1.0;
}
else
{
materialTexture.texture = getTexture(textureFilename);
material.amb = 0.2;
}
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!float(words[3]),
to!float(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());
}
static vec3[] vertPosBuf;
static const(const(vec4)[]) someColors = [
vec4(1,0,0,1)
, vec4(0,1,0,1)
, vec4(0,0,1,1)
, vec4(1,1,0,1)
, vec4(1,0,1,1)
, vec4(0,1,1,1)
];
void render(mat4 mvmat, mat4 pmat, Ray[] skeleton, vec4 color=vec4(1,1,1,1))
{
foreach (iMesh, mesh; meshes)
{
if (vertPosBuf.length < mesh.verts.length)
{
vertPosBuf.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 = skeleton[weight.jointIndex];
pos += (joint.orient * weight.pos + joint.pos) * weight.weightBias;
// Or do it with matrices
//pos += (vec4(weight.pos,1) *
// (mat4.translation(joint.pos.x, joint.pos.y, joint.pos.z)
// * joint.orient.to_matrix!(4,4))
// ).xyz * weight.weightBias;
}
vertPosBuf[vi] = pos;
}
/* 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);
}
/* Draw some triangular outlines */
lines.Vert lineVerts[6] = [
lines.Vert(vec3(0,0,0), someColors[iMesh % someColors.length])
, lines.Vert(vec3(0,0,0), someColors[iMesh % someColors.length])
, lines.Vert(vec3(0,0,0), someColors[iMesh % someColors.length])
, lines.Vert(vec3(0,0,0), someColors[iMesh % someColors.length])
, lines.Vert(vec3(0,0,0), someColors[iMesh % someColors.length])
, lines.Vert(vec3(0,0,0), someColors[iMesh % someColors.length])
];
foreach (tri; mesh.tris)
{
lineVerts[0].pos = vertPosBuf[tri.vi[0]];
lineVerts[1].pos = vertPosBuf[tri.vi[1]];
lineVerts[2].pos = vertPosBuf[tri.vi[1]];
lineVerts[3].pos = vertPosBuf[tri.vi[2]];
lineVerts[4].pos = vertPosBuf[tri.vi[2]];
lineVerts[5].pos = vertPosBuf[tri.vi[0]];
lines.drawLines(lineVerts[]);
}
lines.draw(mvmat, pmat, 2);
}
}
}
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;
size_t numJoints;
static ulong interpolatedSkeletonTime;
static Ray[] interpolatedSkeleton;
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.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)
{
/* TODO draw joint positions? draw joint axes? */
// Draw bones
lines.Vert[2] verts = [
lines.Vert(vec3(0,0,0), vec4(1,0,1,1))
, lines.Vert(vec3(0,0,0), vec4(1,0,1,1))
];
foreach(boneIndex, bone; interpolatedSkeleton)
{
auto parentIndex = model.joints[boneIndex].parentIndex;
if (parentIndex != -1)
{
verts[0].pos = bone.pos;
Ray parentBone = interpolatedSkeleton[parentIndex];
verts[1].pos = parentBone.pos;
lines.drawLines(verts[]);
}
}
lines.draw(mvmat, pmat, 3);
}
/* Right now, render() just renders the outline of each triangle, and will draw
* them in up to six different colors, one for each mesh.
*/
static vec3[] vertPosBuf;
static const(const(vec4)[]) someColors = [
vec4(1,0,0,1)
, vec4(0,1,0,1)
, vec4(0,0,1,1)
, vec4(1,1,0,1)
, vec4(1,0,1,1)
, vec4(0,1,1,1)
];
void render(mat4 mvmat, mat4 pmat, Ray[] skeleton, vec4 color=vec4(1,1,1,1))
{
foreach (iMesh, mesh; model.meshes)
{
if (vertPosBuf.length < mesh.verts.length)
{
vertPosBuf.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 = skeleton[weight.jointIndex];
pos += (joint.orient * weight.pos + joint.pos) * weight.weightBias;
// Or do it with matrices
//pos += (vec4(weight.pos,1) *
// (mat4.translation(joint.pos.x, joint.pos.y, joint.pos.z)
// * joint.orient.to_matrix!(4,4))
// ).xyz * weight.weightBias;
}
vertPosBuf[vi] = pos;
}
/* 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);
}
/* Draw some triangular outlines */
lines.Vert lineVerts[6] = [
lines.Vert(vec3(0,0,0), someColors[iMesh % someColors.length])
, lines.Vert(vec3(0,0,0), someColors[iMesh % someColors.length])
, lines.Vert(vec3(0,0,0), someColors[iMesh % someColors.length])
, lines.Vert(vec3(0,0,0), someColors[iMesh % someColors.length])
, lines.Vert(vec3(0,0,0), someColors[iMesh % someColors.length])
, lines.Vert(vec3(0,0,0), someColors[iMesh % someColors.length])
];
foreach (tri; mesh.tris)
{
lineVerts[0].pos = vertPosBuf[tri.vi[0]];
lineVerts[1].pos = vertPosBuf[tri.vi[1]];
lineVerts[2].pos = vertPosBuf[tri.vi[1]];
lineVerts[3].pos = vertPosBuf[tri.vi[2]];
lineVerts[4].pos = vertPosBuf[tri.vi[2]];
lineVerts[5].pos = vertPosBuf[tri.vi[0]];
lines.drawLines(lineVerts[]);
}
lines.draw(mvmat, pmat, 2);
}
}
// renderGPU()
mat4[] boneMatrices;
bool gpuInitialized;
GLint mvmatUniloc;
GLint pmatUniloc;
GLint boneMatricesUniloc;
GLint colorMapUniloc;
GLint ambUniloc;
GLint uvAttloc;
GLint normalAttloc;
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, vec4 color=vec4(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])
boneMatrices[iBone] = bone.orient.to_matrix!(4,4).translate(bone.pos.x, bone.pos.y, bone.pos.z);
/* 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");
/* Send model-view matrix */
glUniformMatrix4fv(mvmatUniloc, 1, GL_TRUE, mvmat.value_ptr);
glErrorCheck("sent mvmat uniform");
/* Send projection matrix */
glUniformMatrix4fv(pmatUniloc, 1, GL_TRUE, pmat.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");
if (ambUniloc >= 0)
glUniform1f(ambUniloc, mesh.material.amb);
/* Enable our vertex attributes */
// TODO Use VAO
if (uvAttloc >= 0)
glEnableVertexAttribArray(uvAttloc);
if (normalAttloc >= 0)
glEnableVertexAttribArray(normalAttloc);
glEnableVertexAttribArray(boneIndicesAttloc+0);
glEnableVertexAttribArray(weightBiasesAttloc);
glEnableVertexAttribArray(weightPosAttloc+0);
glEnableVertexAttribArray(weightPosAttloc+1);
glEnableVertexAttribArray(weightPosAttloc+2);
glEnableVertexAttribArray(weightPosAttloc+3);
GPUVert VT;
/* Specify our vertex attribute layout (actual data in VBO already) */
foreach (i; 0..4)
glVertexAttribPointer(weightPosAttloc+i, 4, GL_FLOAT, GL_FALSE, VT.sizeof,
cast(void*) (VT.weightPos.offsetof + (i * VT.weightPos[0].sizeof)));
glVertexAttribPointer(weightBiasesAttloc, 4, GL_FLOAT, GL_FALSE, VT.sizeof,
cast(void*) VT.weightBiases.offsetof);
glVertexAttribPointer(boneIndicesAttloc, 4, GL_FLOAT, GL_FALSE, VT.sizeof,
cast(void*) VT.boneIndices.offsetof);
if (uvAttloc >= 0)
glVertexAttribPointer(uvAttloc, 2, GL_FLOAT, GL_FALSE, VT.sizeof,
cast(void*) VT.uv.offsetof);
if (normalAttloc >= 0)
glVertexAttribPointer(normalAttloc, 3, GL_FLOAT, GL_FALSE, VT.sizeof,
cast(void*) VT.normal.offsetof);
/* Set texture sampler for color map TODO use Material better! */
if (colorMapUniloc >= 0)
{
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, mesh.material.texes[0].texture);
glUniform1i(colorMapUniloc, 0);
}
glErrorCheck("md5 9.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 */
if (uvAttloc >= 0)
glDisableVertexAttribArray(uvAttloc);
if (normalAttloc >= 0)
glDisableVertexAttribArray(normalAttloc);
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;
/* Calculate normals for each weight. A "weight" in MD5 parlance is a bone-space
* vertex. To calculate the position of a vertex in the mesh, we must calculate
* a bone space for each bone, transform each weight into this space, and then
* calculated a weighted average of each weight corresponding to a given vertex.
* I'm going to try to do the same now with vertex normals.
*/
/* 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;
/* TODO Calculate weight normals */
model.generateBindPose(iMesh, true);
foreach (iVert, vert; mesh.verts)
{
GPUVert v;
v.uv = vec2(vert.uv.x, vert.uv.y);
v.normal = MD5Model.bindPoseVerts[iVert].normal;
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.boneIndices [iWeight] = cast(uint)weight.jointIndex;
v.weightBiases[iWeight] = cast(float)weight.weightBias;
v.weightPos [iWeight] = vec4(weight.pos.x, weight.pos.y, weight.pos.z, 1f);
}
else
{
v.boneIndices [iWeight] = 0f;
v.weightBiases[iWeight] = 0f;
v.weightPos [iWeight] = vec4(0,0,0,0);
}
}
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)
{
static if (0)
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.boneIndices,
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, mesh.verts.length * data[0].sizeof, data.ptr, GL_STATIC_DRAW);
/* 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);
glErrorCheck("md5 end of initGPU()");
}
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
/* Grab shader variable locations */
mvmatUniloc = md5ShaderProgram.getUniformLocation("viewMatrix");
pmatUniloc = md5ShaderProgram.getUniformLocation("projMatrix");
boneMatricesUniloc = md5ShaderProgram.getUniformLocation("boneMatrices");
colorMapUniloc = md5ShaderProgram.getUniformLocation("colorMap");
ambUniloc = md5ShaderProgram.getUniformLocation("ambU");
uvAttloc = md5ShaderProgram.getAttribLocation ("uvV");
normalAttloc = md5ShaderProgram.getAttribLocation ("normalV");
boneIndicesAttloc = md5ShaderProgram.getAttribLocation ("boneIndices");
weightBiasesAttloc = md5ShaderProgram.getAttribLocation ("weightBiases");
weightPosAttloc = md5ShaderProgram.getAttribLocation ("weightPos[0]");
assert(mvmatUniloc >= 0);
assert(pmatUniloc >= 0);
assert(boneMatricesUniloc >= 0);
//assert(colorMapUniloc >= 0);
//assert(uvAttloc >= 0);
assert(boneIndicesAttloc >= 0);
assert(weightBiasesAttloc >= 0);
assert(weightPosAttloc >= 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), vec3(.2,.2,1));
vertexer.add(outVerts[1], vec2(1,1), vec3(1,1,1), vec3(.2,.2,1));
vertexer.add(outVerts[2], vec2(2,2), vec3(2,2,2), vec3(.2,.2,1));
vertexer.add(outVerts[0], vec2(0,0), vec3(0,0,0), vec3(.2,.2,1));
}
vertexer.draw(shaderProgram, mvmat, pmat, mesh.material, GL_LINES);
}
}
void draw(mat4 mvmat, mat4 pmat, ulong t, vec4 color=vec4(1,1,1,1))
{
if (vertexer is null)
{
vertexer = new Vertexer();
emptyMaterial = new Material();
shaderProgram = new ShaderProgram("vert.glsl", "frag-red.glsl");
shaderProgram1 = new ShaderProgram("vert-color3-uv2--color3-uv2.glsl", "frag-color3-uv2.glsl");
md5ShaderProgram = new ShaderProgram("vert-md5.glsl", "frag-md5.glsl");
}
calculateInterpolatedSkeleton(t);
if (optRenderFull)
{
if (optRenderSoftware)
render(mvmat, pmat, interpolatedSkeleton, color);
else
renderGPU(mvmat, pmat, color);
}
static if (0)
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.
*/
void calculateInterpolatedSkeleton(ulong t)
{
if (interpolatedSkeletonTime == t)
return;
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!
}
interpolatedSkeletonTime = t;
}
}
/* TODO animation sequences instead of just looping the same animation */
class MD5Animator
{
MD5Animation anim;
ulong start; // hnsecs!
this(MD5Animation anim)
{
this.anim = anim;
this.start = GameTime.gt;
}
void draw(mat4 mvmat, mat4 pmat, vec4 color=vec4(1,1,1,1))
{
anim.draw(mvmat, pmat, GameTime.gt-start, color);
}
/* This is a little bypass that allows us to calculate an interpolated skeleton
* without skinning or rendering the model. This is convenient if we need to
* get something from the skeleton before rendering, for instance a camera bone
* position.
*/
void calculateInterpolatedSkeleton()
{
anim.calculateInterpolatedSkeleton(GameTime.gt-start);
}
}
void stop() {
writeln("STOP");
}
#version 150
/* MD5 vertex shader */
uniform mat4 viewMatrix, projMatrix;
uniform mat4 boneMatrices[32];
in vec2 uvV;
in vec4 boneIndices;
in vec4 weightBiases;
in vec4 weightPos[4];
in vec3 normalV;
out vec2 uvF;
out vec3 normalF;
mat3 mat423(mat4 m)
{
return mat3(
m[0][0], m[0][1], m[0][2],
m[1][0], m[1][1], m[1][2],
m[2][0], m[2][1], m[2][2]
);
}
void main()
{
vec4 positionObjectSpace;
vec3 normalObjectSpace;
positionObjectSpace =
boneMatrices[int(boneIndices[0])] * weightPos[0] * weightBiases[0] +
boneMatrices[int(boneIndices[1])] * weightPos[1] * weightBiases[1] +
boneMatrices[int(boneIndices[2])] * weightPos[2] * weightBiases[2] +
boneMatrices[int(boneIndices[3])] * weightPos[3] * weightBiases[3];
normalObjectSpace =
(mat423(boneMatrices[int(boneIndices[0])])) * normalV * weightBiases[0] +
(mat423(boneMatrices[int(boneIndices[1])])) * normalV * weightBiases[1] +
(mat423(boneMatrices[int(boneIndices[2])])) * normalV * weightBiases[2] +
(mat423(boneMatrices[int(boneIndices[3])])) * normalV * weightBiases[3];
gl_Position = projMatrix * (viewMatrix * positionObjectSpace);
normalF = normalObjectSpace;
uvF = uvV;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment