加载和渲染带动画和蒙皮的 Milkshape 3D 模型






4.81/5 (13投票s)
本文介绍了如何使用OpenGL加载、动画化和显示Milkshape ms3d二进制文件。
引言
骨骼动画是一种3D渲染技术,它使用由顶点组成的外部模型和用于动画的内部骨架。为了避免模型变形,每个顶点都分配了一组骨骼和权重,用于根据动画关键帧计算其最终位置。
在现代应用中,这项技术被频繁使用。Autodesk Media and Entertainment 是生产电影、广告和电脑游戏软件的公司之一。在其现有产品中,我们可以找到Maya和3DStudio MAX。max文件格式的文档并不完善,而3DS格式仅用于遗留目的。在某些情况下,如果您从3DSMax导出再重新导入,动画和模型会断裂。Autodesk为开发者提供了SDK支持,以便他们实现自己的导出器和导入器,但这需要大量时间。另一方面,MS3D是一种文档齐全的文件格式,并附有源代码示例,Milkshape 3D是一个非常快速易用的应用程序,因此可以轻松加载和渲染其原生文件格式。所以,如果您想在应用程序中使用3D建模,可以转向Milkshape来创建和动画化您的对象。
代码是用JAVA编写的,但我也有一个使用XNA或DirectX Managed的C#版本。如果您需要代码,请告诉我,我会通过电子邮件发送,或者也许会公开发布。

关于骨骼动画和蒙皮的进一步阅读可以在 http://en.wikipedia.org/wiki/Skeletal_animation 进行。我建议您在开始编写代码之前,先更多地了解这项技术。
本文涵盖的主题
- JOGL (java opengl)
- 骨骼动画
- Milkshape 3D模型加载与渲染
- GLSL
- 硬件蒙皮
规格
该应用程序包含以下功能
- GLSL中的顶点操作
- 加载和渲染Milkshape文件
- 骨骼动画
- OpenGL渲染
技术信息
- 用JAVA Netbeans编写的代码
- 使用JOGL进行渲染
- 使用GLSL进行硬件蒙皮
要求
- Netbeans
- 支持OpenGL视频卡,并支持GLSL(已在Nvidia上测试,ATI和较旧的Nvidia芯片组的硬件蒙皮会失败)
- Milkshape 3D
- JOGL
使用应用程序
应用程序的演示可以在 http://inline.no-ip.org/testing/java/ViewApplet2 (软件蒙皮版本) 或 http://inline.no-ip.org/testing/java/ViewApplet3 (硬件蒙皮版本) 找到。
Using the Code
提供的代码旨在作为Java Applet在支持Java的浏览器中运行。骨骼动画类名为MilkshapeModel
,其构造函数接收一个绝对URL,用于下载所需文件。纹理必须与模型位于同一目录。布尔参数表示硬件蒙皮标志。如果设置为true,则尝试下载并编译用于硬件蒙皮的GLSL VertexShader。
model = new MilkshapeModel(new URL
("https://:8080/WebApplication/dwarf2.ms3d"), drawable.getGL(), true);
基本Milkshape 3D文件格式为
- 标题
- "MS3D000000" 后跟版本号(版本3或4)
- 顶点数据
- 顶点的坐标
- 三角形数据
- 指向顶点的指针,以及表面法线
- 组数据(对象/网格)
- 组名和指向三角形的指针
- 材质数据
- 颜色细节
- 骨骼数据
- 动画数据
支持的文件格式为版本4。模型加载器基于http://chumbalum.swissquake.ch/提供的C++二进制Milkshape加载器示例。
骨骼动画
在此示例中,骨骼动画分两步进行:骨骼设置(计算关键帧插值并更新矩阵)和顶点更新(通过关联的骨骼和权重获得顶点位置)。这可以通过着色器以软件或硬件方式完成。软件方式需要大量的计算能力,并且取决于顶点数量。此外,如果我们在同一场景中添加更多模型,性能会明显下降。着色器执行速度更快,并利用GPU的算力,这样我们就可以将CPU用于其他操作。
在我们的例子中,我们有一个骨骼层次结构,每个骨骼都有自己的关键帧集。这些提供了旋转和平移数据。第一步是找到包含给定帧的两个关键帧,并插值结果。此外,我们需要将结果与父关节连接起来。关节在数组中预先排序,因此当我们评估一个关节时,我们就知道它的父关节已经评估过了。
//bone evaluation
for (int i = 0; i < numJoints; i++) {
EvaluateJoint(i, frame);
}
private void EvaluateJoint(int index, float frame) {
MilkshapeJoint joint = Joints[index];
//
// calculate joint animation matrix, this matrix will animate matLocalSkeleton
//
float[] pos = {0.0f, 0.0f, 0.0f};
int numPositionKeys = (int) joint.positionKeys.length;
if (numPositionKeys > 0) {
int i1 = -1;
int i2 = -1;
// find the two keys, where "frame" is in between for the position channel
for (int i = 0; i < (numPositionKeys - 1); i++) {
if (frame >= joint.positionKeys[i].time &&
frame < joint.positionKeys[i + 1].time) {
i1 = i;
i2 = i + 1;
break;
}
}
// if there are no such keys
if (i1 == -1 || i2 == -1) {
// either take the first
if (frame < joint.positionKeys[0].time) {
pos[0] = joint.positionKeys[0].key[0];
pos[1] = joint.positionKeys[0].key[1];
pos[2] = joint.positionKeys[0].key[2];
} // or the last key
else if (frame >= joint.positionKeys[numPositionKeys - 1].time) {
pos[0] = joint.positionKeys[numPositionKeys - 1].key[0];
pos[1] = joint.positionKeys[numPositionKeys - 1].key[1];
pos[2] = joint.positionKeys[numPositionKeys - 1].key[2];
}
} // there are such keys, so interpolate using hermite interpolation
else {
MilkshapeKeyFrame p0 = joint.positionKeys[i1];
MilkshapeKeyFrame p1 = joint.positionKeys[i2];
MilkshapeTangent m0 = joint.tangents[i1];
MilkshapeTangent m1 = joint.tangents[i2];
// normalize the time between the keys into [0..1]
float t = (frame - joint.positionKeys[i1].time) /
(joint.positionKeys[i2].time - joint.positionKeys[i1].time);
float t2 = t * t;
float t3 = t2 * t;
// calculate hermite basis
float h1 = 2.0f * t3 - 3.0f * t2 + 1.0f;
float h2 = -2.0f * t3 + 3.0f * t2;
float h3 = t3 - 2.0f * t2 + t;
float h4 = t3 - t2;
// do hermite interpolation
pos[0] = h1 * p0.key[0] + h3 * m0.tangentOut[0] +
h2 * p1.key[0] + h4 * m1.tangentIn[0];
pos[1] = h1 * p0.key[1] + h3 * m0.tangentOut[1] +
h2 * p1.key[1] + h4 * m1.tangentIn[1];
pos[2] = h1 * p0.key[2] + h3 * m0.tangentOut[2] +
h2 * p1.key[2] + h4 * m1.tangentIn[2];
}
}
float[] quat = {0.0f, 0.0f, 0.0f, 1.0f};
int numRotationKeys = (int) joint.rotationKeys.length;
if (numRotationKeys > 0) {
int i1 = -1;
int i2 = -1;
// find the two keys, where "frame" is in between for the rotation channel
for (int i = 0; i < (numRotationKeys - 1); i++) {
if (frame >= joint.rotationKeys[i].time &&
frame < joint.rotationKeys[i + 1].time) {
i1 = i;
i2 = i + 1;
break;
}
}
// if there are no such keys
if (i1 == -1 || i2 == -1) {
// either take the first key
if (frame < joint.rotationKeys[0].time) {
AngleQuaternion(joint.rotationKeys[0].key, quat);
} // or the last key
else if (frame >= joint.rotationKeys[numRotationKeys - 1].time) {
AngleQuaternion(joint.rotationKeys[numRotationKeys - 1].key, quat);
}
} // there are such keys, so do the quaternion slerp interpolation
else {
float t = (frame - joint.rotationKeys[i1].time) /
(joint.rotationKeys[i2].time - joint.rotationKeys[i1].time);
float[] q1 = new float[4];
AngleQuaternion(joint.rotationKeys[i1].key, q1);
float[] q2 = new float[4];
AngleQuaternion(joint.rotationKeys[i2].key, q2);
QuaternionSlerp(q1, q2, t, quat);
}
}
// make a matrix from pos/quat
float matAnimate[][] = new float[3][4];
QuaternionMatrix(quat, matAnimate);
matAnimate[0][3] = pos[0];
matAnimate[1][3] = pos[1];
matAnimate[2][3] = pos[2];
// animate the local joint matrix using: matLocal = matLocalSkeleton * matAnimate
RecConcatTransforms(joint.matLocalSkeleton, matAnimate, joint.matLocal);
// build up the hierarchy if joints
// matGlobal = matGlobal(parent) * matLocal
if (joint.parentIndex == -1) {
//memcpy(joint.matGlobal, joint.matLocal, sizeof(joint.matGlobal));
for (int k = 0; k < joint.matLocal.length; k++) {
System.arraycopy(joint.matLocal[k], 0,
joint.matGlobal[k], 0, joint.matLocal[k].length);
}
} else {
MilkshapeJoint parentJoint = Joints[joint.parentIndex];
RecConcatTransforms(parentJoint.matGlobal, joint.matLocal, joint.matGlobal);
}
}
private void RecConcatTransforms(float in1[][], float in2[][], float out[][]) {
out[0][0] = in1[0][0] * in2[0][0] + in1[0][1] *
in2[1][0] + in1[0][2] * in2[2][0];
out[0][1] = in1[0][0] * in2[0][1] + in1[0][1] * in2[1][1] + in1[0][2] * in2[2][1];
out[0][2] = in1[0][0] * in2[0][2] + in1[0][1] * in2[1][2] + in1[0][2] * in2[2][2];
out[0][3] = in1[0][0] * in2[0][3] + in1[0][1] *
in2[1][3] + in1[0][2] * in2[2][3] + in1[0][3];
out[1][0] = in1[1][0] * in2[0][0] + in1[1][1] * in2[1][0] + in1[1][2] * in2[2][0];
out[1][1] = in1[1][0] * in2[0][1] + in1[1][1] * in2[1][1] + in1[1][2] * in2[2][1];
out[1][2] = in1[1][0] * in2[0][2] + in1[1][1] * in2[1][2] + in1[1][2] * in2[2][2];
out[1][3] = in1[1][0] * in2[0][3] + in1[1][1] *
in2[1][3] + in1[1][2] * in2[2][3] + in1[1][3];
out[2][0] = in1[2][0] * in2[0][0] + in1[2][1] * in2[1][0] + in1[2][2] * in2[2][0];
out[2][1] = in1[2][0] * in2[0][1] + in1[2][1] * in2[1][1] + in1[2][2] * in2[2][1];
out[2][2] = in1[2][0] * in2[0][2] + in1[2][1] * in2[1][2] + in1[2][2] * in2[2][2];
out[2][3] = in1[2][0] * in2[0][3] + in1[2][1] *
in2[1][3] + in1[2][2] * in2[2][3] + in1[2][3];
}
硬件蒙皮
之后,每个顶点都需要根据骨骼和权重进行变换。Milkshape每个顶点最多使用4个骨骼,这非常适合我们的顶点着色器,因为我们可以将骨骼ID和骨骼权重存储在两个vec4结构中作为顶点属性。
GLSL顶点着色器
attribute vec3 normal;
attribute vec4 weight;
attribute vec4 index;
attribute float numBones;
uniform mat4 matGlobal[100];
uniform mat4 matGlobalSkeleton[100];
uniform vec4 color;
uniform vec4 lightPos;
float DotProduct(in vec4 x, in vec4 y) {
return x[0] * y[0] + x[1] * y[1] + x[2] * y[2];
}
void VectorRotate(in vec4 in1, in mat4 in2, out vec4 rez) {
rez[0] = DotProduct(in1, in2[0]);
rez[1] = DotProduct(in1, in2[1]);
rez[2] = DotProduct(in1, in2[2]);
}
void VectorIRotate(in vec4 in1, in mat4 in2, out vec4 rez) {
rez[0] = in1[0] * in2[0][0] + in1[1] * in2[1][0] + in1[2] * in2[2][0];
rez[1] = in1[0] * in2[0][1] + in1[1] * in2[1][1] + in1[2] * in2[2][1];
rez[2] = in1[0] * in2[0][2] + in1[1] * in2[1][2] + in1[2] * in2[2][2];
}
void VectorTransform(in vec4 in1,in mat4 in2, out vec4 rez) {
rez[0] = DotProduct(in1, in2[0]) + in2[0][3];
rez[1] = DotProduct(in1, in2[1]) + in2[1][3];
rez[2] = DotProduct(in1, in2[2]) + in2[2][3];
}
void VectorITransform(in vec4 in1, in mat4 in2, out vec4 rez) {
vec4 tmp;
tmp[0] = in1[0] - in2[0][3];
tmp[1] = in1[1] - in2[1][3];
tmp[2] = in1[2] - in2[2][3];
VectorIRotate(tmp, in2, rez);
}
void main(){
vec4 transformedPosition = vec4(0.0);
vec3 transformedNormal = vec3(0.0);
vec4 curIndex = index;
vec4 curWeight = weight;
for (int i = 0; i < int(numBones); i++)
{
mat4 g44 = matGlobal[int(curIndex.x)];
mat4 s44 = matGlobalSkeleton[int(curIndex.x)];
vec4 vert = vec4(0);
vec4 norm = vec4(0);
vec4 tmpNorm = vec4(0);
vec4 tmpVert = vec4(0);
VectorITransform(gl_Vertex, s44, tmpVert);
VectorTransform(tmpVert, g44, vert);//g
vert[3]=1;
transformedPosition += vert * curWeight.x;
vec4 preNormal=vec4(normal.xyz,1);
VectorIRotate(preNormal, s44, tmpNorm);
VectorRotate(tmpNorm, g44, norm);
norm[3]=1;
transformedNormal += vec3(norm.xyz) * curWeight.x;
// shiftam sa avem urmatoarele valori pentru pondere si indice de os
curIndex = curIndex.yzwx;
curWeight = curWeight.yzwx;
}
gl_Position = gl_ModelViewProjectionMatrix * transformedPosition;
transformedNormal = normalize(transformedNormal);
gl_FrontColor = dot(transformedNormal, lightPos.xyz) * color;
}
上面的代码也有软件版本,供那些没有GLSL支持的用户使用。
要了解更多关于GL
SL
的信息,请尝试 http://www.opengl.org/registry/doc/GLSLangSpec.4.00.8.clean.pdf。GLSL仅用于演示目的。它处理得不好,并且在uniform矩阵上浪费了很多内存,因此在许多显卡上都会失败。但是,如果您想测试它,请使用骨骼较少的模型,并根据您的需求调整矩阵大小。
我们使用AdvanceAnimation
来动画化模型。
if (lastTick == 0) {
lastTick = Calendar.getInstance().getTimeInMillis();
}
long currentTick = Calendar.getInstance().getTimeInMillis();
long delta = currentTick - lastTick;
lastTick = currentTick;
//model.AnimationFPS=120;
model.AdvanceAnimation((float) delta / 1000);
结论
本文是对使用Java和Milkshape3D模型加载进行OpenGL软件和硬件蒙皮的介绍。它并非经过优化,但您可以在您的应用程序中大量使用它。希望这对您有所帮助,如果您对代码进行了任何改进,请告诉我 :)。