OpenGL导入3DMax模型(.3DS)

146 篇文章 34 订阅

参考

 

源代码:

解释:

CLoad3DS.h为加载3DMax模型的头文件,CLoad3DS.cpp为加载3DMax模型的实现文件,

nehewidget.h为Qt下使用OpenGL头文件,nehewidget.cpp为Qt下使用OpenGL实现文件。

注意:

1.3D模型和纹理图片资源需要放在源代码同一目录下的Data目录中,即/Data/3DS和/Data/pic下。

2.图标和其他纹理图片存放在Resources文件夹下。

CLoad3DS.h:

#ifndef _CLoad3DS_h_
#define _CLoad3DS_h_
 
#include <windows.h>
#include <cassert>
#include <cmath>
#include <string>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <vector>                 
 
#include <olectl.h>             
#include <cmath> 
#include <ctime>
#include <algorithm>
#include <direct.h>
 
//初始化OpenGL环境
#include <gl/gl.h>
#include <gl/glu.h>
#include <gl/glut.h>
 
#pragma   comment(lib,"opengl32.lib")
#pragma   comment(lib,"glu32.lib")
 
#define PICPATH "\\Data\\pic\\"  //纹理资源的地址
 
// 基本块(Primary Chunk),位于文件的开始
#define PRIMARY 0x4D4D
 
// 主块(Main Chunks)
#define OBJECTINFO 0x3D3D        // 网格对象的版本号
#define VERSION 0x0002           // .3ds文件的版本
#define EDITKEYFRAME 0xB000      // 所有关键帧信息的头部
 
// 对象的次级定义(包括对象的材质和对象)
#define MATERIAL   0xAFFF        // 保存纹理信息
#define OBJECT     0x4000        // 保存对象的面、顶点等信息
 
// 材质的次级定义
#define MATNAME 0xA000        // 保存材质名称
#define MATDIFFUSE 0xA020     // 对象/材质的颜色
#define MATMAP 0xA200         // 新材质的头部
#define MATMAPFILE 0xA300     // 保存纹理的文件名
 
#define OBJECT_MESH 0x4100    // 新的网格对象
 
// OBJECT_MESH的次级定义
#define OBJECT_VERTICES 0x4110      // 对象顶点
#define OBJECT_FACES    0x4120      // 对象的面
#define OBJECT_MATERIAL    0x4130   // 对象的材质
#define OBJECT_UV      0x4140       // 对象的UV纹理坐标
 
// 下面的宏定义计算一个矢量的长度
#define Mag(Normal) (sqrt(Normal.x*Normal.x + Normal.y*Normal.y + Normal.z*Normal.z))
#define MAX_TEXTURES 100            // 最大的纹理数目
 
using namespace std;
class NBVector3
{
public:
    NBVector3() {}
    NBVector3(float X, float Y, float Z)
    {
        x = X; y = Y; z = Z;
    }
    inline NBVector3 operator+(NBVector3 vVector)
    {
        return NBVector3(vVector.x + x, vVector.y + y, vVector.z + z);
    }
    inline NBVector3 operator-(NBVector3 vVector)
    {
        return NBVector3(x - vVector.x, y - vVector.y, z - vVector.z);
    }
    inline NBVector3 operator-()
    {
        return NBVector3(-x, -y, -z);
    }
    inline NBVector3 operator*(float num)
    {
        return NBVector3(x * num, y * num, z * num);
    }
    inline NBVector3 operator/(float num)
    {
        return NBVector3(x / num, y / num, z / num);
    }
    inline NBVector3 operator^(const NBVector3 &rhs) const
    {
        return NBVector3(y * rhs.z - rhs.y * z, rhs.x * z - x * rhs.z, x * rhs.y - rhs.x * y);
    }
    union
    {
        struct
        {
            float x;
            float y;
            float z;
        };
        float v[3];
    };             
};
 
// 定义2D点类,用于保存模型的UV纹理坐标
class CVector2
{
public:
    float x, y;
};
 
// 面的结构定义
struct tFace
{
    int vertIndex[3];      // 顶点索引
    int coordIndex[3];     // 纹理坐标索引
};
 
// 材质信息结构体
struct tMaterialInfo
{
    char strName[255];      // 纹理名称
    char strFile[255];      // 如果存在纹理映射,则表示纹理文件名称
    BYTE color[3];          // 对象的RGB颜色
    int texureId;           // 纹理ID
    float uTile;            // u 重复
    float vTile;            // v 重复
    float uOffset;          // u 纹理偏移
    float vOffset;          // v 纹理偏移
} ;
 
// 对象信息结构体
struct t3DObject
{
    int numOfVerts;      // 模型中顶点的数目
    int numOfFaces;      // 模型中面的数目
    int numTexVertex;    // 模型中纹理坐标的数目
    int materialID;      // 纹理ID
    bool bHasTexture;    // 是否具有纹理映射
    char strName[255];   // 对象的名称
    NBVector3 *pVerts;   // 对象的顶点
    NBVector3 *pNormals; // 对象的法向量
    CVector2 *pTexVerts; // 纹理UV坐标
    tFace *pFaces;       // 对象的面信息
};
 
// 模型信息结构体
struct t3DModel
{
    UINT texture[MAX_TEXTURES];
    int numOfObjects;                  // 模型中对象的数目
    int numOfMaterials;                // 模型中材质的数目
    vector<tMaterialInfo> pMaterials;  // 材质链表信息
    vector<t3DObject> pObject;         // 模型中对象链表信息
};
 
struct tIndices
{             
    unsigned short a, b, c, bVisible; 
};
 
// 保存块信息的结构
struct tChunk
{
    unsigned short int ID;           // 块的ID   
    unsigned int length;             // 块的长度
    unsigned int bytesRead;          // 需要读的块数据的字节数
};
 
typedef struct tagBoundingBoxStruct
{
    NBVector3  BoxPosMaxVertex;
    NBVector3  BoxNegMaxVertex;
} BoundingBoxVertex2;
 
// 下面的函数求两点决定的矢量
NBVector3 Vector(NBVector3 vPoint1, NBVector3 vPoint2);
// 下面的函数两个矢量相加
NBVector3 AddVector(NBVector3 vVector1, NBVector3 vVector2);
// 下面的函数处理矢量的缩放
NBVector3 DivideVectorByScaler(NBVector3 vVector1, float Scaler);
// 下面的函数返回两个矢量的叉积
NBVector3 Cross(NBVector3 vVector1, NBVector3 vVector2);
// 下面的函数归一化矢量
NBVector3 Normalize(NBVector3 vNormal);
 
//
#define FRAND   (((float)rand()-(float)rand())/RAND_MAX)
#define Clamp(x, min, max)  x = (x<min  ? min : x<max ? x : max);
#define SQUARE(x)  (x)*(x)
struct vector3_t
{
    vector3_t(float x, float y, float z) : x(x), y(y), z(z) {}
    vector3_t(const vector3_t &v) : x(v.x), y(v.y), z(v.z) {}
    vector3_t() : x(0.0f), y(0.0f), z(0.0f) {}
 
    vector3_t& operator=(const vector3_t &rhs)
    {
        x = rhs.x;
        y = rhs.y;
        z = rhs.z;
        return *this;
    }
 
    // vector add
    vector3_t operator+(const vector3_t &rhs) const
    {
        return vector3_t(x + rhs.x, y + rhs.y, z + rhs.z);
    }
 
    // vector subtract
    vector3_t operator-(const vector3_t &rhs) const
    {
        return vector3_t(x - rhs.x, y - rhs.y, z - rhs.z);
    }
 
    // scalar multiplication
    vector3_t operator*(const float scalar) const
    {
        return vector3_t(x * scalar, y * scalar, z * scalar);
    }
 
    // dot product
    float operator*(const vector3_t &rhs) const
    {
        return x * rhs.x + y * rhs.y + z * rhs.z;
    }
 
    // cross product
    vector3_t operator^(const vector3_t &rhs) const
    {
        return vector3_t(y * rhs.z - rhs.y * z, rhs.x * z - x * rhs.z, x * rhs.y - rhs.x * y);
    }
 
    float& operator[](int index)
    {
        return v[index];
    }
 
    float Length()
    {
        float length = (float)sqrt(SQUARE(x) + SQUARE(y) + SQUARE(z));
        return (length != 0.0f) ? length : 1.0f;
    }
 
    /*****************************************************************************
    Normalize()
 
    Helper function to normalize vectors
    *****************************************************************************/
    vector3_t Normalize()
    {
        *this = *this * (1.0f/Length());
        return *this;
    }
 
    union
    {
        struct
        {
            float x;
            float y;
            float z;
        };
        float v[3];
    };
};
 
// CLoad3DS类处理所有的装入代码
class CLoad3DS
{
public:
    CLoad3DS();                // 初始化数据成员
    // 装入3ds文件到模型结构中
    bool Import3DS(t3DModel *pModel, char *strFileName);
 
private:
    // 读入一个纹理
    int BuildTexture(char *szPathName, GLuint &texid);
    // 读一个字符串
    int GetString(char *);
    // 读下一个块
    void ReadChunk(tChunk *);
    // 读下一个块
    void ProcessNextChunk(t3DModel *pModel, tChunk *);
    // 读下一个对象块
    void ProcessNextObjectChunk(t3DModel *pModel, t3DObject *pObject, tChunk *);
    // 读下一个材质块
    void ProcessNextMaterialChunk(t3DModel *pModel, tChunk *);
    // 读对象颜色的RGB值
    void ReadColorChunk(tMaterialInfo *pMaterial, tChunk *pChunk);
    // 读对象的顶点
    void ReadVertices(t3DObject *pObject, tChunk *);
    // 读对象的面信息
    void ReadVertexIndices(t3DObject *pObject, tChunk *);
    // 读对象的纹理坐标
    void ReadUVCoordinates(t3DObject *pObject, tChunk *);
    // 读赋予对象的材质名称
    void ReadObjectMaterial(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk);
    // 计算对象顶点的法向量
    void ComputeNormals(t3DModel *pModel);
    // 关闭文件,释放内存空间
    void CleanUp();
    // 文件指针
    FILE *m_FilePointer;
 
    tChunk *m_CurrentChunk;
    tChunk *m_TempChunk;
};
void changeObject(float trans[10]);
void drawModel(t3DModel Model,bool touming,bool outTex);
#endif

CLoad3DS.cpp:

#include "CLoad3DS.h"
 
#pragma warning (disable: 4996)
 
// 下面的函数求两点决定的矢量
NBVector3 Vector(NBVector3 vPoint1, NBVector3 vPoint2)
{
    NBVector3 vVector;             
     
    vVector.x = vPoint1.x - vPoint2.x;     
    vVector.y = vPoint1.y - vPoint2.y;     
    vVector.z = vPoint1.z - vPoint2.z;     
     
    return vVector;               
}
 
// 下面的函数两个矢量相加
NBVector3 AddVector(NBVector3 vVector1, NBVector3 vVector2)
{
    NBVector3 vResult;             
    vResult.x = vVector2.x + vVector1.x;   
    vResult.y = vVector2.y + vVector1.y;   
    vResult.z = vVector2.z + vVector1.z;   
    return vResult;               
}
 
// 下面的函数处理矢量的缩放
NBVector3 DivideVectorByScaler(NBVector3 vVector1, float Scaler)
{
    NBVector3 vResult;             
    vResult.x = vVector1.x / Scaler;     
    vResult.y = vVector1.y / Scaler;     
    vResult.z = vVector1.z / Scaler;       
    return vResult;               
}
 
// 下面的函数返回两个矢量的叉积
NBVector3 Cross(NBVector3 vVector1, NBVector3 vVector2)
{
    NBVector3 vCross;                  
    vCross.x = ((vVector1.y * vVector2.z) - (vVector1.z * vVector2.y));
    vCross.y = ((vVector1.z * vVector2.x) - (vVector1.x * vVector2.z));
    vCross.z = ((vVector1.x * vVector2.y) - (vVector1.y * vVector2.x));
    return vCross;               
}
 
// 下面的函数归一化矢量
NBVector3 Normalize(NBVector3 vNormal)
{
    double Magnitude;              
    Magnitude = Mag(vNormal);          // 获得矢量的长度  
    vNormal.x /= (float)Magnitude;       
    vNormal.y /= (float)Magnitude;       
    vNormal.z /= (float)Magnitude;         
    return vNormal;               
}
 
// 读入一个纹理
int CLoad3DS::BuildTexture(char *szPathName, GLuint &texid)
{
    HDC      hdcTemp;                        // The DC To Hold Our Bitmap
    HBITMAP    hbmpTemp;                     // Holds The Bitmap Temporarily
    LPPICTURE  pPicture;                     // IPicture Interface,此处较参考网址的博主的代码做了修改
    OLECHAR    wszPath[MAX_PATH+1];          // Full Path To Picture (WCHAR)
    char    szPath[MAX_PATH+1];         // Full Path To Picture
    long    lWidth;                          // Width In Logical Units
    long    lHeight;                         // Height In Logical Units
    long    lWidthPixels;                    // Width In Pixels
    long    lHeightPixels;                   // Height In Pixels
    GLint    glMaxTexDim ;                   // Holds Maximum Texture Size
 
    if (strstr(szPathName, "http://"))       // If PathName Contains http:// Then...
    {
        strcpy(szPath, szPathName);          // Append The PathName To szPath
    }
    else                                     // Otherwise... We Are Loading From A File
    {
        getcwd(szPath,MAX_PATH);              // Get Our Working Directory,此处较参考网址的博主的代码做了修改
        strcat(szPath, PICPATH);              // Append "\" After The Working Directory
        strcat(szPath, szPathName);           // Append The PathName
    }
     
    MultiByteToWideChar(CP_ACP, 0, szPath, -1, wszPath, MAX_PATH);    // Convert From ASCII To Unicode
    HRESULT hr = OleLoadPicturePath(wszPath, 0, 0, 0, IID_IPicture, reinterpret_cast<LPVOID *>(&pPicture));//此处较参考网址的博主的代码做了修改
 
    if(FAILED(hr))                             // If Loading Failed
        return FALSE;                          // Return False
 
    hdcTemp = CreateCompatibleDC(GetDC(0));    // Create The Windows Compatible Device Context
    if(!hdcTemp)                               // Did Creation Fail?
    {
        pPicture->Release();                   // Decrements IPicture Reference Count
        return FALSE;                          // Return False (Failure)
    }
 
    glGetIntegerv(GL_MAX_TEXTURE_SIZE, &glMaxTexDim);   // Get Maximum Texture Size Supported
 
    pPicture->get_Width(&lWidth);                       // Get IPicture Width (Convert To Pixels)
    lWidthPixels  = MulDiv(lWidth, GetDeviceCaps(hdcTemp, LOGPIXELSX), 2540);
    pPicture->get_Height(&lHeight);                     // Get IPicture Height (Convert To Pixels)
    lHeightPixels  = MulDiv(lHeight, GetDeviceCaps(hdcTemp, LOGPIXELSY), 2540);
 
    // Resize Image To Closest Power Of Two
    if (lWidthPixels <= glMaxTexDim) // Is Image Width Less Than Or Equal To Cards Limit
        lWidthPixels = 1 << (int)floor((log((double)lWidthPixels)/log(2.0f)) + 0.5f);
    else // Otherwise Set Width To "Max Power Of Two" That The Card Can Handle
        lWidthPixels = glMaxTexDim;
 
    if (lHeightPixels <= glMaxTexDim) // Is Image Height Greater Than Cards Limit
        lHeightPixels = 1 << (int)floor((log((double)lHeightPixels)/log(2.0f)) + 0.5f);
    else // Otherwise Set Height To "Max Power Of Two" That The Card Can Handle
        lHeightPixels = glMaxTexDim;
 
    //  Create A Temporary Bitmap
    BITMAPINFO  bi = {0};                       // The Type Of Bitmap We Request
    DWORD    *pBits = 0;                        // Pointer To The Bitmap Bits
 
    bi.bmiHeader.biSize      = sizeof(BITMAPINFOHEADER); // Set Structure Size
    bi.bmiHeader.biBitCount  = 32;                       // 32 Bit
    bi.bmiHeader.biWidth     = lWidthPixels;             // Power Of Two Width
    bi.bmiHeader.biHeight    = lHeightPixels;            // Make Image Top Up (Positive Y-Axis)
    bi.bmiHeader.biCompression  = BI_RGB;                // RGB Encoding
    bi.bmiHeader.biPlanes    = 1;                        // 1 Bitplane
 
    //  Creating A Bitmap This Way Allows Us To Specify Color Depth And Gives Us Imediate Access To The Bits
    hbmpTemp = CreateDIBSection(hdcTemp, &bi, DIB_RGB_COLORS, (void**)&pBits, 0, 0);
 
    if(!hbmpTemp)                                 // Did Creation Fail?
    {
        DeleteDC(hdcTemp);                        // Delete The Device Context
        pPicture->Release();                      // Decrements IPicture Reference Count
        return FALSE;                             // Return False (Failure)
    }
 
    SelectObject(hdcTemp, hbmpTemp);              // Select Handle To Our Temp DC And Our Temp Bitmap Object
 
    // Render The IPicture On To The Bitmap
    pPicture->Render(hdcTemp, 0, 0, lWidthPixels, lHeightPixels, 0, lHeight, lWidth, -lHeight, 0);
 
    // Convert From BGR To RGB Format And Add An Alpha Value Of 255
    for(long i = 0; i < lWidthPixels * lHeightPixels; i++)        // Loop Through All Of The Pixels
    {
        BYTE* pPixel  = (BYTE*)(&pBits[i]);              // Grab The Current Pixel
        BYTE temp    = pPixel[0];                        // Store 1st Color In Temp Variable (Blue)
        pPixel[0]    = pPixel[2];                        // Move Red Value To Correct Position (1st)
        pPixel[2]    = temp;                             // Move Temp Value To Correct Blue Position (3rd)
 
        // This Will Make Any Black Pixels, Completely Transparent    (You Can Hardcode The Value If You Wish)
        if ((pPixel[0]==0) && (pPixel[1]==0) && (pPixel[2]==0))      // Is Pixel Completely Black
            pPixel[3]  = 0;                         // Set The Alpha Value To 0
        else                                        // Otherwise
            pPixel[3]  = 255;                       // Set The Alpha Value To 255
    }
 
    glGenTextures(1, &texid);                       // Create The Texture
 
    // Typical Texture Generation Using Data From The Bitmap
    glBindTexture(GL_TEXTURE_2D, texid);                            // Bind To The Texture ID
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); // (Modify This For The Type Of Filtering You Want)
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); // (Modify This For The Type Of Filtering You Want)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, lWidthPixels, lHeightPixels, 0, GL_RGBA, GL_UNSIGNED_BYTE, pBits);  // (Modify This If You Want Mipmaps)
 
    DeleteObject(hbmpTemp);                        // Delete The Object
    DeleteDC(hdcTemp);                             // Delete The Device Context
 
    pPicture->Release();                           // Decrements IPicture Reference Count
 
    printf( "load %s!" , szPath );
    // Return True (All is okay)
    return TRUE;  
}
 
// 构造函数的功能是初始化tChunk数据
CLoad3DS::CLoad3DS()
{
    m_CurrentChunk = new tChunk;        // 初始化并为当前的块分配空间
    m_TempChunk = new tChunk;          // 初始化一个临时块并分配空间
}
 
// 打开一个3ds文件,读出其中的内容,并释放内存
bool CLoad3DS::Import3DS(t3DModel *pModel, char *strFileName)
{
    char strMessage[255] = {0};
 
    // 打开一个3ds文件
    m_FilePointer = fopen(strFileName,"rb");
 
    // 确保所获得的文件指针合法
    if(!m_FilePointer)
    {
        sprintf(strMessage, "Unable to find the file: %s!", strFileName);
        MessageBox(NULL, (LPWSTR)strMessage, (LPWSTR)"Error", MB_OK);
        return false;
    }
 
    // 当文件打开之后,首先应该将文件最开始的数据块读出以判断是否是一个3ds文件
    // 如果是3ds文件的话,第一个块ID应该是PRIMARY
 
    // 将文件的第一块读出并判断是否是3ds文件
    ReadChunk(m_CurrentChunk);
 
    // 确保是3ds文件
    if (m_CurrentChunk->ID != PRIMARY)
    {
        sprintf(strMessage, "Unable to load PRIMARY chuck from file: %s!", strFileName);
        MessageBox(NULL, (LPWSTR)strMessage, (LPWSTR)"Error", MB_OK);
        return false;
    }
 
    // 现在开始读入数据,ProcessNextChunk()是一个递归函数
    // 通过调用下面的递归函数,将对象读出
    ProcessNextChunk(pModel, m_CurrentChunk);
 
    // 在读完整个3ds文件之后,计算顶点的法线
    ComputeNormals(pModel);
 
    // 释放内存空间
    CleanUp();
 
    return true;
}
 
// 下面的函数释放所有的内存空间,并关闭文件
void CLoad3DS::CleanUp()
{
 
    fclose(m_FilePointer);            // 关闭当前的文件指针
    delete m_CurrentChunk;            // 释放当前块
    delete m_TempChunk;              // 释放临时块
}
 
// 下面的函数读出3ds文件的主要部分
void CLoad3DS::ProcessNextChunk(t3DModel *pModel, tChunk *pPreviousChunk)
{
    t3DObject newObject = {0};          // 用来添加到对象链表
    tMaterialInfo newTexture = {0};     // 用来添加到材质链表
    unsigned int version = 0;           // 保存文件版本
    int buffer[50000] = {0};            // 用来跳过不需要的数据
 
    m_CurrentChunk = new tChunk;        // 为新的块分配空间   
 
    // 下面每读一个新块,都要判断一下块的ID,如果该块是需要的读入的,则继续进行
    // 如果是不需要读入的块,则略过
 
    // 继续读入子块,直到达到预定的长度
    while (pPreviousChunk->bytesRead < pPreviousChunk->length)
    {
        // 读入下一个块
        ReadChunk(m_CurrentChunk);
 
        // 判断块的ID号
        switch (m_CurrentChunk->ID)
        {
        case VERSION:              // 文件版本号
 
            // 在该块中有一个无符号短整型数保存了文件的版本
 
            // 读入文件的版本号,并将字节数添加到bytesRead变量中
            m_CurrentChunk->bytesRead += fread(&version, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
 
            // 如果文件版本号大于3,给出一个警告信息
            if (version > 0x03)
                MessageBox(NULL, (LPWSTR)"This 3DS file is over version 3 so it may load incorrectly", (LPWSTR)"Warning", MB_OK);
            break;
 
        case OBJECTINFO:            // 网格版本信息
 
            // 读入下一个块
            ReadChunk(m_TempChunk);
 
            // 获得网格的版本号
            m_TempChunk->bytesRead += fread(&version, 1, m_TempChunk->length - m_TempChunk->bytesRead, m_FilePointer);
 
            // 增加读入的字节数
            m_CurrentChunk->bytesRead += m_TempChunk->bytesRead;
 
            // 进入下一个块
            ProcessNextChunk(pModel, m_CurrentChunk);
            break;
 
        case MATERIAL:              // 材质信息
 
            // 材质的数目递增
            pModel->numOfMaterials++;
 
            // 在纹理链表中添加一个空白纹理结构
            pModel->pMaterials.push_back(newTexture);
 
            // 进入材质装入函数
            ProcessNextMaterialChunk(pModel, m_CurrentChunk);
            break;
 
        case OBJECT:              // 对象的名称
 
            // 该块是对象信息块的头部,保存了对象了名称
 
            // 对象数递增
            pModel->numOfObjects++;
 
            // 添加一个新的tObject节点到对象链表中
            pModel->pObject.push_back(newObject);
 
            // 初始化对象和它的所有数据成员
            memset(&(pModel->pObject[pModel->numOfObjects - 1]), 0, sizeof(t3DObject));
 
            // 获得并保存对象的名称,然后增加读入的字节数
            m_CurrentChunk->bytesRead += GetString(pModel->pObject[pModel->numOfObjects - 1].strName);
 
            // 进入其余的对象信息的读入
            ProcessNextObjectChunk(pModel, &(pModel->pObject[pModel->numOfObjects - 1]), m_CurrentChunk);
            break;
 
        case EDITKEYFRAME:
 
            // 跳过关键帧块的读入,增加需要读入的字节数
            m_CurrentChunk->bytesRead += fread(buffer, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
            break;
 
        default:
 
            // 跳过所有忽略的块的内容的读入,增加需要读入的字节数
            m_CurrentChunk->bytesRead += fread(buffer, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
            break;
        }
 
        // 增加从最后块读入的字节数
        pPreviousChunk->bytesRead += m_CurrentChunk->bytesRead;
    }
 
    // 释放当前块的内存空间
    delete m_CurrentChunk;
    m_CurrentChunk = pPreviousChunk;
}
 
// 下面的函数处理所有的文件中对象的信息
void CLoad3DS::ProcessNextObjectChunk(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk)
{
    int buffer[50000] = {0};          // 用于读入不需要的数据
 
    // 对新的块分配存储空间
    m_CurrentChunk = new tChunk;
 
    // 继续读入块的内容直至本子块结束
    while (pPreviousChunk->bytesRead < pPreviousChunk->length)
    {
        // 读入下一个块
        ReadChunk(m_CurrentChunk);
 
        // 区别读入是哪种块
        switch (m_CurrentChunk->ID)
        {
        case OBJECT_MESH:          // 正读入的是一个新块
 
            // 使用递归函数调用,处理该新块
            ProcessNextObjectChunk(pModel, pObject, m_CurrentChunk);
            break;
 
        case OBJECT_VERTICES:        // 读入是对象顶点
            ReadVertices(pObject, m_CurrentChunk);
            break;
 
        case OBJECT_FACES:          // 读入的是对象的面
            ReadVertexIndices(pObject, m_CurrentChunk);
            break;
 
        case OBJECT_MATERIAL:        // 读入的是对象的材质名称
 
            // 该块保存了对象材质的名称,可能是一个颜色,也可能是一个纹理映射。同时在该块中也保存了
            // 纹理对象所赋予的面
 
            // 下面读入对象的材质名称
            ReadObjectMaterial(pModel, pObject, m_CurrentChunk);     
            break;
 
        case OBJECT_UV:            // 读入对象的UV纹理坐标
 
            // 读入对象的UV纹理坐标
            ReadUVCoordinates(pObject, m_CurrentChunk);
            break;
 
        default:
 
            // 略过不需要读入的块
            m_CurrentChunk->bytesRead += fread(buffer, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
            break;
        }
 
        // 添加从最后块中读入的字节数到前面的读入的字节中
        pPreviousChunk->bytesRead += m_CurrentChunk->bytesRead;
    }
 
    // 释放当前块的内存空间,并把当前块设置为前面块
    delete m_CurrentChunk;
    m_CurrentChunk = pPreviousChunk;
}
 
// 下面的函数处理所有的材质信息
void CLoad3DS::ProcessNextMaterialChunk(t3DModel *pModel, tChunk *pPreviousChunk)
{
    int buffer[50000] = {0};          // 用于读入不需要的数据
 
    // 给当前块分配存储空间
    m_CurrentChunk = new tChunk;
 
    // 继续读入这些块,知道该子块结束
    while (pPreviousChunk->bytesRead < pPreviousChunk->length)
    {
        // 读入下一块
        ReadChunk(m_CurrentChunk);
 
        // 判断读入的是什么块
        switch (m_CurrentChunk->ID)
        {
        case MATNAME:              // 材质的名称
 
            // 读入材质的名称
            m_CurrentChunk->bytesRead += fread(pModel->pMaterials[pModel->numOfMaterials - 1].strName, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
            break;
 
        case MATDIFFUSE:            // 对象的R G B颜色
            ReadColorChunk(&(pModel->pMaterials[pModel->numOfMaterials - 1]), m_CurrentChunk);
            break;
 
        case MATMAP:              // 纹理信息的头部
 
            // 进入下一个材质块信息
            ProcessNextMaterialChunk(pModel, m_CurrentChunk);
            break;
 
        case MATMAPFILE:            // 材质文件的名称
 
            // 读入材质的文件名称
            m_CurrentChunk->bytesRead += fread(pModel->pMaterials[pModel->numOfMaterials - 1].strFile, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
            break;
 
        default:
 
            // 掠过不需要读入的块
            m_CurrentChunk->bytesRead += fread(buffer, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
            break;
        }
 
        // 添加从最后块中读入的字节数
        pPreviousChunk->bytesRead += m_CurrentChunk->bytesRead;
    }
 
    // 删除当前块,并将当前块设置为前面的块
    delete m_CurrentChunk;
    m_CurrentChunk = pPreviousChunk;
}
 
// 下面函数读入块的ID号和它的字节长度
void CLoad3DS::ReadChunk(tChunk *pChunk)
{
    // 读入块的ID号,占用了2个字节。块的ID号象OBJECT或MATERIAL一样,说明了在块中所包含的内容
    pChunk->bytesRead = fread(&pChunk->ID, 1, 2, m_FilePointer);
 
    // 然后读入块占用的长度,包含了四个字节
    pChunk->bytesRead += fread(&pChunk->length, 1, 4, m_FilePointer);
}
 
// 下面的函数读入一个字符串
int CLoad3DS::GetString(char *pBuffer)
{
    int index = 0;
 
    // 读入一个字节的数据
    fread(pBuffer, 1, 1, m_FilePointer);
 
    // 直到结束
    while (*(pBuffer + index++) != 0) {
 
        // 读入一个字符直到NULL
        fread(pBuffer + index, 1, 1, m_FilePointer);
    }
 
    // 返回字符串的长度
    return strlen(pBuffer) + 1;
}
 
// 下面的函数读入RGB颜色
void CLoad3DS::ReadColorChunk(tMaterialInfo *pMaterial, tChunk *pChunk)
{
    // 读入颜色块信息
    ReadChunk(m_TempChunk);
 
    // 读入RGB颜色
    m_TempChunk->bytesRead += fread(pMaterial->color, 1, m_TempChunk->length - m_TempChunk->bytesRead, m_FilePointer);
 
    // 增加读入的字节数
    pChunk->bytesRead += m_TempChunk->bytesRead;
}
 
// 下面的函数读入顶点索引
void CLoad3DS::ReadVertexIndices(t3DObject *pObject, tChunk *pPreviousChunk)
{
    unsigned short index = 0;  // 用于读入当前面的索引
 
    // 读入该对象中面的数目
    pPreviousChunk->bytesRead += fread(&pObject->numOfFaces, 1, 2, m_FilePointer);
 
    // 分配所有面的存储空间,并初始化结构
    pObject->pFaces = new tFace [pObject->numOfFaces];
    memset(pObject->pFaces, 0, sizeof(tFace) * pObject->numOfFaces);
 
    // 遍历对象中所有的面
    for(int i = 0; i < pObject->numOfFaces; i++)
    {
        for(int j = 0; j < 4; j++)
        {
            // 读入当前面的第一个点
            pPreviousChunk->bytesRead += fread(&index, 1, sizeof(index), m_FilePointer);
 
            if(j < 3)
            {
                // 将索引保存在面的结构中
                pObject->pFaces[i].vertIndex[j] = index;
            }
        }
    }
}
 
// 下面的函数读入对象的UV坐标
void CLoad3DS::ReadUVCoordinates(t3DObject *pObject, tChunk *pPreviousChunk)
{
    // 为了读入对象的UV坐标,首先需要读入UV坐标的数量,然后才读入具体的数据
 
    // 读入UV坐标的数量
    pPreviousChunk->bytesRead += fread(&pObject->numTexVertex, 1, 2, m_FilePointer);
 
    // 分配保存UV坐标的内存空间
    pObject->pTexVerts = new CVector2 [pObject->numTexVertex];
 
    // 读入纹理坐标
    pPreviousChunk->bytesRead += fread(pObject->pTexVerts, 1, pPreviousChunk->length - pPreviousChunk->bytesRead, m_FilePointer);
}
 
// 读入对象的顶点
void CLoad3DS::ReadVertices(t3DObject *pObject, tChunk *pPreviousChunk)
{
    // 在读入实际的顶点之前,首先必须确定需要读入多少个顶点。
 
    // 读入顶点的数目
    pPreviousChunk->bytesRead += fread(&(pObject->numOfVerts), 1, 2, m_FilePointer);
 
    // 分配顶点的存储空间,然后初始化结构体
    pObject->pVerts = new NBVector3 [pObject->numOfVerts];
    memset(pObject->pVerts, 0, sizeof(NBVector3) * pObject->numOfVerts);
 
    // 读入顶点序列
    pPreviousChunk->bytesRead += fread(pObject->pVerts, 1, pPreviousChunk->length - pPreviousChunk->bytesRead, m_FilePointer);
 
    // 现在已经读入了所有的顶点。
    // 因为3D Studio Max的模型的Z轴是指向上的,因此需要将y轴和z轴翻转过来。
    // 具体的做法是将Y轴和Z轴交换,然后将Z轴反向。
 
    // 遍历所有的顶点
    for(int i = 0; i < pObject->numOfVerts; i++)
    {
        // 保存Y轴的值
        float fTempY = pObject->pVerts[i].y;
 
        // 设置Y轴的值等于Z轴的值
        pObject->pVerts[i].y = pObject->pVerts[i].z;
 
        // 设置Z轴的值等于-Y轴的值
        pObject->pVerts[i].z = -fTempY;
    }
}
 
// 下面的函数读入对象的材质名称
void CLoad3DS::ReadObjectMaterial(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk)
{
    char strMaterial[255] = {0};      // 用来保存对象的材质名称
    int buffer[50000] = {0};        // 用来读入不需要的数据
 
    // 材质或者是颜色,或者是对象的纹理,也可能保存了象明亮度、发光度等信息。
 
    // 下面读入赋予当前对象的材质名称
    pPreviousChunk->bytesRead += GetString(strMaterial);
 
    // 遍历所有的纹理
    for(int i = 0; i < pModel->numOfMaterials; i++)
    {
        //如果读入的纹理与当前的纹理名称匹配
        if(strcmp(strMaterial, pModel->pMaterials[i].strName) == 0)
        {
            // 设置材质ID
            pObject->materialID = i;
 
            // 判断是否是纹理映射,如果strFile是一个长度大于1的字符串,则是纹理
            if(strlen(pModel->pMaterials[i].strFile) > 0) {
                 
                //载入纹理
                BuildTexture(pModel->pMaterials[i].strFile, pModel->texture[pObject->materialID]);
                // 设置对象的纹理映射标志
                pObject->bHasTexture = true;
 
                char strMessage[100];
                sprintf(strMessage, "file name : %s!", pModel->pMaterials[i].strFile);
                printf( "%s\n" , strMessage );
//              MessageBox(NULL, strMessage, "Error", MB_OK);
            } 
            break;
        }
        else
        {
            // 如果该对象没有材质,则设置ID为-1
            pObject->materialID = -1;
        }
    }
 
    pPreviousChunk->bytesRead += fread(buffer, 1, pPreviousChunk->length - pPreviousChunk->bytesRead, m_FilePointer);
}     
 
// 下面的这些函数主要用来计算顶点的法向量,顶点的法向量主要用来计算光照
// 下面的函数用于计算对象的法向量
void CLoad3DS::ComputeNormals(t3DModel *pModel)
{
    NBVector3 vVector1, vVector2, vNormal, vPoly[3];
 
    // 如果模型中没有对象,则返回
    if(pModel->numOfObjects <= 0)
        return;
 
    // 遍历模型中所有的对象
    for(int index = 0; index < pModel->numOfObjects; index++)
    {
        // 获得当前的对象
        t3DObject *pObject = &(pModel->pObject[index]);
 
        // 分配需要的存储空间
        NBVector3 *pNormals    = new NBVector3 [pObject->numOfFaces];
        NBVector3 *pTempNormals  = new NBVector3 [pObject->numOfFaces];
        pObject->pNormals    = new NBVector3 [pObject->numOfVerts];
        int i=0;
        // 遍历对象的所有面
        for(i=0; i < pObject->numOfFaces; i++)
        {                       
            vPoly[0] = pObject->pVerts[pObject->pFaces[i].vertIndex[0]];
            vPoly[1] = pObject->pVerts[pObject->pFaces[i].vertIndex[1]];
            vPoly[2] = pObject->pVerts[pObject->pFaces[i].vertIndex[2]];
 
            // 计算面的法向量
 
            vVector1 = Vector(vPoly[0], vPoly[2]);    // 获得多边形的矢量
            vVector2 = Vector(vPoly[2], vPoly[1]);    // 获得多边形的第二个矢量
 
            vNormal = Cross(vVector1, vVector2);    // 获得两个矢量的叉积
            pTempNormals[i] = vNormal;          // 保存非规范化法向量
            vNormal = Normalize(vNormal);        // 规范化获得的叉积
 
            pNormals[i] = vNormal;            // 将法向量添加到法向量列表中
        }
 
        // 下面求顶点法向量
        NBVector3 vSum (0.0, 0.0, 0.0);
        NBVector3 vZero = vSum;
        int shared=0;
        // 遍历所有的顶点
        for (i = 0; i < pObject->numOfVerts; i++)     
        {
            for (int j = 0; j < pObject->numOfFaces; j++)  // 遍历所有的三角形面
            {                        // 判断该点是否与其它的面共享
                if (pObject->pFaces[j].vertIndex[0] == i ||
                    pObject->pFaces[j].vertIndex[1] == i ||
                    pObject->pFaces[j].vertIndex[2] == i)
                {
                    vSum = AddVector(vSum, pTempNormals[j]);
                    shared++;               
                }
            }
 
            pObject->pNormals[i] = DivideVectorByScaler(vSum, float(-shared));
 
            // 规范化最后的顶点法向
            pObject->pNormals[i] = Normalize(pObject->pNormals[i]); 
 
            vSum = vZero;               
            shared = 0;                   
        }
 
        // 释放存储空间,开始下一个对象
        delete [] pTempNormals;
        delete [] pNormals;
    }
}
// 对模型进行移动变换等操作
void changeObject(float trans[10])
{
    glTranslatef(trans[0],trans[1],trans[2]);
    glScalef(trans[3],trans[4],trans[5]);
    glRotatef(trans[6],trans[7],trans[8],trans[9]);
}
// 绘制模型
void drawModel(t3DModel Model,bool touming,bool outTex)
{  
    if( touming ){
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        glColor4f(1,1,1,0.5);
    }
     
    for(int i = 0; i < Model.numOfObjects; i++)
    {
        t3DObject *pObject = &Model.pObject[i];
        if(!outTex)
        {
            if(pObject->bHasTexture)
            {
                glEnable(GL_TEXTURE_2D);
                 
                glBindTexture(GL_TEXTURE_2D, Model.texture[pObject->materialID]);
            }
            else
            {
                glDisable(GL_TEXTURE_2D);
                glColor3ub(255, 255, 255);
            }
        }
        glBegin(GL_TRIANGLES);         
        for(int j = 0; j < pObject->numOfFaces; j++)
        {
            for(int whichVertex = 0; whichVertex < 3; whichVertex++)
            {
                int index = pObject->pFaces[j].vertIndex[whichVertex];
                glNormal3f(pObject->pNormals[ index ].x, pObject->pNormals[ index ].y, pObject->pNormals[ index ].z);
                if(pObject->bHasTexture)
                {
                    if(pObject->pTexVerts)
                    {
                        glColor3f(1.0,1.0,1.0);
                        glTexCoord2f(pObject->pTexVerts[ index ].x, pObject->pTexVerts[ index ].y);
                    }
                }
                else
                {                  
                    if(Model.pMaterials.size() && pObject->materialID >= 0)
                    {
                        BYTE *pColor = Model.pMaterials[pObject->materialID].color;
                        glColor3ub(pColor[0], pColor[1], pColor[2]);
                    }
                }
                glVertex3f(pObject->pVerts[ index ].x, pObject->pVerts[ index ].y, pObject->pVerts[ index ].z);
            }      
        }      
        glEnd();
    }
    if( touming )
        glDisable(GL_BLEND);
}

 

nehewidget.h:

#ifndef NEHEWIDGET_H
#define NEHEWIDGET_H
 
#include <QGLWidget>
#include <QKeyEvent>
#include <QTimer>
#include <QImage>
#include <QMessageBox>
#include <QIcon>
#include <QDebug>
#include <GL/glu.h>
#include <CLoad3DS.h>
 
class NeHeWidget : public QGLWidget
{
    Q_OBJECT
public:
    explicit NeHeWidget(QWidget *parent = 0);
    ~NeHeWidget();
 
protected:
    void initializeGL();
    void paintGL();
    void resizeGL(int w, int h);
    void keyPressEvent(QKeyEvent *);
 
private:
    bool fullscreen;
    float xRot,yRot,zRot;
    GLuint texture[3];
 
    void initLight();
    void drawObject();
    void init3DMAX();
 
signals:
 
public slots:
 
};
 
#endif // NEHEWIDGET_H

nehewidget.cpp:

#include "nehewidget.h"
 
GLfloat lightAmbient[4] = { 0.5, 0.5, 0.5, 1.0 };
GLfloat lightDiffuse[4] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat lightPosition[4] = { 0.0, 0.0, 2.0, 1.0 };
 
CLoad3DS *gothicLoader=new CLoad3DS;
t3DModel gothicModel;
 
NeHeWidget::NeHeWidget(QWidget *parent) :
    QGLWidget(parent)
{
    this->setWindowTitle(tr("OpenGL加载3DS模型"));
    this->setWindowIcon(QIcon(":/Textures/Resources/Desert.jpg"));
    resize(640,480);
    xRot = 0.0;
    zRot = 0.0;
    yRot = 0.0;
}
 
NeHeWidget::~NeHeWidget()
{
 
}
 
//*************************************
// 作    者:  朱兴宇
// 时    间:  2015/4/16 15:08
// 权    限:  protected
// 返    回:  void
// 方法说明:  OpenGL初始化
//*************************************
void NeHeWidget::initializeGL()
{
    //使用阴影平滑
    glShadeModel(GL_SMOOTH);
    //黑色清屏r,g,b,alpha
    glClearColor(0.0,0.0,0.0,0.0);
    //设置深度缓存
    glClearDepth(1.0);
    //启用深度缓存
    glEnable(GL_DEPTH_TEST);
    //启用深度测试的类型
    glDepthFunc(GL_LEQUAL);
    //使用透视
    glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_NICEST);
    //初始化灯光
    initLight();
    //初始化3D模型
    init3DMAX();
}
 
//*************************************
// 作    者:  朱兴宇
// 时    间:  2015/4/16 15:07
// 权    限:  protected
// 返    回:  void
// 方法说明:  窗口大小变化
//*************************************
void NeHeWidget::resizeGL(int w, int h)
{
    if(h == 0)
    {
        h = 1;
    }                                   // Far
    //重置当前视口
    glViewport(0,0,(GLint)w,(GLint)h);
    //选择投影矩阵
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(60.0, (GLfloat)w/(GLfloat)h, 1.0, 100.0);//建立透视投影矩阵
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
 
//*************************************
// 作    者:  朱兴宇
// 时    间:  2015/4/16 15:17
// 权    限:  protected
// 返    回:  void
// 方法说明:  绘制
//*************************************
void NeHeWidget::paintGL()
{
    //清楚屏幕颜色和深度
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    drawObject();
}
 
//*************************************
// 作    者:  朱兴宇
// 时    间:  2015/4/16 15:06
// 权    限:  private
// 返    回:  void
// 方法说明:  绘制物体
//*************************************
void NeHeWidget::drawObject()
{
    glLoadIdentity();
    glTranslatef(0.0,0.0,-30.0);
    glRotatef(xRot,1.0,0.0,0.0);
    glRotatef(yRot,0.0,1.0,0.0);
    glTranslatef(0.0,-3.0,0.0);
    glScaled(0.25,0.25,0.25);
    drawModel(gothicModel,true,false); 
 
    glBlendFunc( GL_SRC_ALPHA, GL_ONE );
}
 
//*************************************
// 作    者:  朱兴宇
// 时    间:  2015/4/16 15:10
// 权    限:  protected
// 返    回:  void
// 方法说明:  鼠标响应事件
//*************************************
void NeHeWidget::keyPressEvent(QKeyEvent * e)
{
    switch(e->key())
    {
    case Qt::Key_F2:
        fullscreen = !fullscreen;
        if(fullscreen)
        {
            showFullScreen();
        }
        else
        {
            showNormal();
            resize(640,480);
        }
        updateGL();
        break;
    case Qt::Key_Escape:
        close();
        break;
    case Qt::Key_Up:
        xRot+= 5.0;
        if (xRot>=360)
        {
            xRot=0;
        }
        updateGL();
        break;
    case Qt::Key_Down:
        xRot -= 5.0;
        if (xRot<=0)
        {
            xRot=360;
        }
        updateGL();
        break;
    case Qt::Key_Right:
        yRot += 5.0;
        if (yRot>=360)
        {
            yRot=0;
        }
        updateGL();
        break;
    case Qt::Key_Left:
        yRot -= 5.0;
        if (yRot<=0)
        {
            yRot=360;
        }
        updateGL();
        break;
    //启用灯光
    case Qt::Key_L:
    {
        static bool choose = false;
        if(choose)
        {
            glDisable(GL_LIGHTING);
            choose = false;
        }
        else
        {
            glEnable(GL_LIGHTING);
            choose = true;
        }
        updateGL();
    }
        break;
    //启用混合
    case Qt::Key_B:
    {
       static bool blend = !blend;
         if ( blend )
         {
            glEnable(GL_BLEND);
            glDisable(GL_DEPTH_TEST);
            blend = false;
         }
         else
         {
            glDisable(GL_BLEND);
            glEnable(GL_DEPTH_TEST);
            blend = true;
         }
         updateGL();
    }
        break;
    }
     
}
 
//*************************************
// 作    者:  朱兴宇
// 时    间:  2015/4/16 15:07
// 权    限:  private
// 返    回:  void
// 方法说明:  初始化灯光
//*************************************
void NeHeWidget::initLight()
{
    glLightfv( GL_LIGHT1, GL_AMBIENT, lightAmbient );
    glLightfv( GL_LIGHT1, GL_DIFFUSE, lightDiffuse );
    glLightfv( GL_LIGHT1, GL_POSITION, lightPosition );
    glEnable( GL_LIGHT1 );
}
 
//*************************************
// 作    者:  朱兴宇
// 时    间:  2015/4/16 22:00
// 权    限:  private
// 返    回:  void
// 方法说明:  初始化3DMAX导入模型
//*************************************
void NeHeWidget::init3DMAX()
{
    //导入模型 模型的文件夹尽量这样设置  
    //然后模型贴图 装在Data/3DS里面,一定要跟前面截图的文件夹名字一样,想改得去CLoad3DS文件里面改 
    gothicLoader->Import3DS(&gothicModel, "Data/3DS/GUTEMB_L.3DS");
}

main.cpp:

#include "nehewidget.h"
#include <QTextCodec>
#include <QtGui/QApplication>
 
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    {
        QTextCodec *codec = QTextCodec::codecForName("utf8");
        QTextCodec::setCodecForLocale(codec);
        QTextCodec::setCodecForCStrings(codec);
        QTextCodec::setCodecForTr(codec);
    }
    NeHeWidget* w=new NeHeWidget;
    w->show();
    return a.exec();
}

 

dinlou.qrc:

<RCC>
    <qresource prefix="/Textures">
      <file>Resources/text.bmp</file>
      <file>Resources/Desert.jpg</file>
    </qresource>
</RCC>

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值