65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (13投票s)

2011年1月20日

CPOL

5分钟阅读

viewsIcon

93154

downloadIcon

4634

本文介绍了如何使用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#版本。如果您需要代码,请告诉我,我会通过电子邮件发送,或者也许会公开发布。

milkshape_models.png

关于骨骼动画和蒙皮的进一步阅读可以在 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支持的用户使用。

要了解更多关于GLSL的信息,请尝试 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软件和硬件蒙皮的介绍。它并非经过优化,但您可以在您的应用程序中大量使用它。希望这对您有所帮助,如果您对代码进行了任何改进,请告诉我 :)。

© . All rights reserved.