V tomto článku si ukážeme, jak nahrát a vykreslit model ve formátu .3DS (3D Studio Max). Náš kód bude umět bez problémů načítat soubory do třetí verze programu, s vyššími verzemi bude pracovat také, ale nebude podporovat jejich nové funkce. Vycházím z ukázkového příkladu z www.gametutorials.com, kde také najdete zdrojové kódy pro C++ (článek je v Delphi).
Hned na úvod bude dobré říct, že 3D Studio Max nemám a modely, které jsem zkoušel, jsou exportované z programu Cinema 4D. Už při testování jsem narazil na rozdíly ve formátu - např. původní model ukládá barvu jako 3x byte, Cinema 4D jako 3x single, proto nemůžu zaručit 100% kompatibilitu.
Program je postaven na NeHe kódu z posledních lekcí. Provedl jsem jen některé drobné úpravy jako např. zavedení proměnných pro rozměry okna atp., ale nebudu ho tu podrobně popisovat. Zaměřím se hlavně na načítání 3ds souboru.
Vše potřebné se nachází v jednotce f_3ds.pas. Na začátku definujeme konstanty, které představují identifikátory jednotlivých bloků v souboru. Každý 3ds soubor se skládá z určitých částí - bloků, které v sobě uchovávají různé informace o modelu. Každý blok obsahuje identifikátor, svoji délku a vlastní data. Některé bloky slouží pouze jako kontejnery a obsahují větší či menší počet jiných bloků. Ne všechny jsou však zdokumentované, ale to nevadí, protože takové bloky je možné díky znalosti jejich délky přeskočit. Podrobný popis struktury souboru se nachází v přiložené dokumentaci.
unit f_3ds;
interface
const// Konstanty hlaviček jednotlivých bloků
PRIMARY = $4D4D;
OBJECTINFO = $3D3D;
VERSION = $0002;
EDITKEYFRAME = $B000;
MATERIAL = $AFFF;
OBJEKT = $4000;
MATNAME = $A000;
MATDIFFUSE = $A020;
MATMAP = $A200;
MATMAPFILE = $A300;
OBJECT_MESH = $4100;
OBJECT_VERTICES = $4110;
OBJECT_FACES = $4120;
OBJECT_MATERIAL = $4130;
OBJECT_UV = $4140;
Dále definujeme několik struktur. CVector3 uchovává souřadnice vertexu, CVector2 ukládá texturové koordináty.
type
CVector3 = record// Vektor 3D
x, y, z: single;
end;
CVector2 = record// Vektor 2D
x, y: single;
end;
Struktura tFace obsahuje informace o plošce (trojúhelníku) objektu. Používají se dvě pole indexů, index do pole vertexů a index do pole texturových koordinátů. Každý vertex je v souboru uložen pouze jednou a informace o plošce obsahuje pouze indexy jednotlivých vrcholů. Odpadá tak dublování vertexů, protože ty jsou často sdíleny více trojúhelníky. Stejné je to i s texturovými koordináty.
tFace = record// Informace o ploškách (trojúhelnících) objektu
vertIndex: array [0..2] of integer;// Index do pole vertexů
coordIndex: array [0..2] of integer;// Index do pole texturových koordinátů
end;
Struktura tMaterialInfo obsahuje informace o materiálu, jeho jméno, cesta k souboru s texturou (pokud existuje), počet bytů použitých pro vyjádření barvy, pole pro barvu, identifikátor textury a další proměnné pro práci s texturou, které ovšem nejsou v kódu využity.
tMaterialInfo = record// Informace o materiálu
strName: string;// Jméno materiálu
strFile: string;// Cesta k souboru s texturou
bpc: integer;// Počet bytů na barvu
colorub: array [0..2] of byte;// Barva v bytech
colorf: array [0..2] of single;// Barva v singlech
texureId: integer;// ID textury
uTile: double;// Opakování textury v ose u (nepoužito)
vTile: double;// Opakování textury v ose v (nepoužito)
uOffset: double;// Posunutí textury v ose u (nepoužito)
vOffset: double;// Posunutí textury v ose v (nepoužito)
end;
Struktura t3DObject obsahuje informace o objektu - počet vertexů, počet plošek (trojúhelníků), počet texturových koordinátů, identifikátor použitého materiálu, flag textury (ano/ne), jméno objektu, pole vertexů, pole normál, pole texturových koordinátů a pole plošek.
t3DObject = record// Informace o objektu
numOfVerts: integer;// Počet vertexů
numOfFaces: integer;// Počet plošek
numTexVertex: integer;// Počet texturových koordinátů
materialID: integer;// ID materiálu
bHasTexture: boolean;// TRUE, pokud materiál obsahuje texturu
strName: string;// Jméno objektu
pVerts: array of CVector3;// Vertexy
pNormals: array of CVector3;// Normály
pTexVerts: array of CVector2;// Texturové koordináty
pFaces: array of tFace;// Plošky
end;
Pt3DObject = ^t3DObject;// Ukazatel na objekt
Struktura t3DModel obsahuje informaci o modelu - počet objektů, počet materiálů, pole materiálů a pole objektů.
t3DModel = record// Informace o modelu
numOfObjects: integer;// Počet objektů
numOfMaterials: integer;// Počet materiálů
pMaterials: array of tMaterialInfo;// Pole materiálů
pObject: array of t3DObject;// Pole objektů
end;
Struktura tChunk obsahuje informace o načítaném bloku. Identifikátor, délku a počet již přečtených bytů.
tChunk = record// Informace o bloku
ID: word;// Identifikátor bloku
length: cardinal;// Délka bloku
bytesRead: cardinal;// Již přečtené byty
end;
Dále následuje třída, která se stará o nahrávání souboru. Myslím, že komentáře u jednotlivých řádků hovoří za vše. K jednotlivým metodám se podrobně vrátím níže.
CLoad3DS = class// Třída pro nahrání 3DS souboru
public
constructor Create;// Konstruktor
destructor Destroy; override;// Destruktor
function Import3DS(var pModel: t3DModel; strFileName: string): boolean// Funkce pro nahrání souboru
private
m_FilePointer: integer;// Ukazatel na soubor
function GetString(var pBuffer: string): integer;// Načte řetězec
procedure ReadChunk(var pChunk: tChunk);// Načte další blok
procedure ProcessNextChunk(var pModel: t3DModel; var pPreviousChunk: tChunk);// Načte další soubor bloků
procedure ProcessNextObjectChunk(var pModel: t3DModel; var pObject: t3DObject; var pPreviousChunk: tChunk);// Načte další objektový blok
procedure ProcessNextMaterialChunk(var pModel: t3DModel; var pPreviousChunk: tChunk);// Načte další materiálový blok
procedure ReadColorChunk(var pMaterial: tMaterialInfo; var pChunk: tChunk);// Načte RGB barvu objektu
procedure ReadVertices(var pObject: t3DObject; var pPreviousChunk: tChunk);// Načte vertexy objektu
procedure ReadVertexIndices(var pObject: t3DObject; var pPreviousChunk: tChunk);// Načte plošky objektu
procedure ReadUVCoordinates(var pObject: t3DObject; var pPreviousChunk: tChunk);// Načte texturové koordináty objektu
procedure ReadObjectMaterial(pModel: t3DModel; var pObject: t3DObject; var pPreviousChunk: tChunk);// Načte materiál objektu
procedure ComputeNormals(pModel: t3DModel);// Vypočítá normály
procedure CleanUp;// Uvolní prostředky a uzavře soubor
end;
Proměnná gBuffer slouží pro načtení nepotřebných dat, jako jsou neznámé bloky nebo bloky, které záměrně přeskočíme, protože je nebudeme potřebovat. Dále přidáme dvě jednotky.
var
gBuffer: array [0..50000] of integer;// Buffer pro načtení nepotřebných dat
implementation
uses SysUtils, Windows;
Funkce Vector spočítá vektor mezi dvěma body. Nic složitého. Jak jistě všichni znáte z analytické geometrie, od koncového bodu se odečte počátek.
function Vector(vPoint1, vPoint2: CVector3): CVector3;// Výpočet vektoru mezi dvěma body
var
vVector: CVector3;
begin
vVector.x := vPoint1.x - vPoint2.x;// Bod 1 - Bod 2
vVector.y := vPoint1.y - vPoint2.y;
vVector.z := vPoint1.z - vPoint2.z;
Result := vVector;
end;
Funce Cross vrací vektorový součin dvou vektorů, tedy vektor kolmý na rovinu, kterou vytvářejí dva původní vektory. S touto funkcí budeme počítat normálové vektory pro světlo.
function Cross(vVector1, vVector2: CVector3): CVector3;// Vektorový součin
var
vCross: CVector3;
begin
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));
Result := vCross;
end;
Funkce Normalize vrací jednotkový vektor.
function Normalize(vNormal: CVector3): CVector3;// Normalizace vektoru
var
Magnitude: Double;
begin
Magnitude := Sqrt(Sqr(vNormal.x) + Sqr(vNormal.y) + Sqr(vNormal.z));// Velikost vektoru
vNormal.x := vNormal.x / Magnitude;// Vektor / velikost
vNormal.y := vNormal.y / Magnitude;
vNormal.z := vNormal.z / Magnitude;
Result := vNormal;
end;
Funkce AddVector vrací součet dvou vektorů.
function AddVector(vVector1, vVector2: CVector3): CVector3;// Součet vektorů
var
vResult: CVector3;
begin
vResult.x := vVector2.x + vVector1.x;// Vektor 1 + Vektor 2
vResult.y := vVector2.y + vVector1.y;
vResult.z := vVector2.z + vVector1.z;
Result := vResult;
end;
Funkce DivideVectorByScaler vydělí vektor číslem a tím ho zkrátí popř. prodlouží.
function DivideVectorByScaler(vVector1: CVector3; Scaler: Double): CVector3;// Dělení vektoru číslem
var
vResult: CVector3;
begin
vResult.x := vVector1.x / Scaler;
vResult.y := vVector1.y / Scaler;
vResult.z := vVector1.z / Scaler;
Result := vResult;
end;
Dále následují vlastní metody třídy CLoad3DS. V proceduře CleanUp se provádí veškerý potřebný úklid - uvolnění paměti, uzavření souborů...
{ CLoad3DS }
procedure CLoad3DS.CleanUp;// Úklid
begin
if m_FilePointer <> -1 then// Máme otevřen soubor?
begin
FileClose(m_FilePointer);// Zavřeme ho
m_FilePointer := -1;
end;
end;
Procedura ComputeNormals slouží pro výpočet normál vertexů. Na začátku zkontrolujeme, zda máme v modelu alespoň jeden objekt. Pokud ne, nemá smysl pokračovat a proceduru ukončíme. V cyklu projdeme všechny objekty modelu.
procedure CLoad3DS.ComputeNormals(pModel: t3DModel);// Výpočet normál
var
vVector1, vVector2, vNormal: CVector3;
vPoly: array [0..2] of CVector3;
index, i, j: integer;
pObject: Pt3DObject;
pNormals, pTempNormals: array of CVector3;
vSum, vZero: CVector3;
shared: integer;
begin
if pModel.numOfObjects <= 0 then exit;// Pokud nemáme objekt tak končíme
for index := 0 to pModel.numOfObjects - 1 do// Cyklus přes všechny objekty modelu
begin
Uložíme si objekt do pomocné proměnné a nastavíme velikost polí pro výpočet normál.
pObject := @pModel.pObject[index];// Získá aktuální objekt
SetLength(pNormals, pObject.numOfFaces);// Alokace potřebné paměti
SetLength(pTempNormals, pObject.numOfFaces);
SetLength(pObject.pNormals, pObject.numOfVerts);
Projdeme všechny plošky objektu. Pro přehlednost si uložíme každý vertex trojúhelníku do samostatné proměnné a vypočítáme normálu plošky (získáme dva vektory a z nich spočítáme normálu). Ještě než z normály uděláme jednotkový vektor, uložíme ji do pomocného pole.
for i := 0 to pObject.numOfFaces - 1 do// Cyklus přes všechny plošky objektu
begin
vPoly[0] := pObject.pVerts[pObject.pFaces[i].vertIndex[0]];// Pro přehlednost uloží 3 vertexy do pomocné proměnné
vPoly[1] := pObject.pVerts[pObject.pFaces[i].vertIndex[1]];
vPoly[2] := pObject.pVerts[pObject.pFaces[i].vertIndex[2]];
// Vlastní výpočet normál
vVector1 := Vector(vPoly[0], vPoly[2]);// Výpočet 1. vektoru
vVector2 := Vector(vPoly[2], vPoly[1]);// Výpočet 2. vektoru
vNormal := Cross(vVector1, vVector2);// Výpočet normály
pTempNormals[i] := vNormal;// Uloží nenormalizovanou normálu pro pozdější výpočty normál vertexů
vNormal := Normalize(vNormal);// Normalizuje normálu
pNormals[i] := vNormal;// Uloží normálu do pole
end;
Teď zbývá jen vypočítat normály jednotlivých vertexů. V prvním cyklu projdeme všechny vertexy a v dalším zjistíme, ke kolika ploškám daný vertex náleží. Pro každý vertex projdeme všechny plošky a pokud najdeme nějakou plošku, ke které patří, zvýšíme "váhu" příslušného vertexu o normálu plošky (použijeme dříve uložený normálový vektor, před tím, než jsme z něj udělali jednotkový). Nakonec z normálového vektoru vertexu uděláme jednotkový. Není to tak hrozné, jak to z předchozího textu vypadá. Vše je vidět na obrázku:
// Výpočet normál vertexů
ZeroMemory(@vSum, sizeof(vSum));
vZero := vSum;
shared := 0;
for i := 0 to pObject.numOfVerts - 1 do// Cyklus přes všechny vertexy
begin
for j := 0 to pObject.numOfFaces - 1 do// Cyklus přes všechny plošky (trojúhelníky)
if (pObject.pFaces[j].vertIndex[0] = i) or// Je vertex sdílen s jinou ploškou?
(pObject.pFaces[j].vertIndex[1] = i) or
(pObject.pFaces[j].vertIndex[2] = i) then
begin
vSum := AddVector(vSum, pTempNormals[j]);// Přičte nenormalizovanou normálu
Inc(shared);// Zvýší počet sdílených trojúhelníků
end;
pObject.pNormals[i] := DivideVectorByScaler(vSum, -shared);// Získá normálu
pObject.pNormals[i] := Normalize(pObject.pNormals[i]);// Normalizuje normálu
vSum := vZero;
shared := 0;
end;
SetLength(pTempNormals, 0);// Uvolní paměť
SetLength(pNormals, 0);
end;
end;
Konstruktor - zde se inicializují proměnné.
constructor CLoad3DS.Create;// Konstruktor
begin
m_FilePointer := -1;
end;
Destruktor - zde se uvolňují prostředky používané proměnnými.
destructor CLoad3DS.Destroy;// Destruktor
begin
inherited;
end;
Funkce GetString načte řetězec ze souboru do proměnné pBuffer a vrací jeho délku. Princip je jednoduchý, čteme znak po znaku, dokud nenarazíme na konec řetězce (#0).
function CLoad3DS.GetString(var pBuffer: string): integer;// Načte řetězec
var
index: integer;
tmpChar: Char;
begin
index := 1;
FileRead(m_FilePointer, tmpChar, 1);// Načte první znak
pBuffer := pBuffer + tmpChar;
while pBuffer[index] <> #0 do// Kontroluje, zda jsme na konci řetězce (#0)
begin
FileRead(m_FilePointer, tmpChar, 1);// Načte další znak
pBuffer := pBuffer + tmpChar;
Inc(index);
end;
Result := Length(pBuffer);// Vrací délku řetězce
end;
Funkce Import3DS nahraje model ze souboru. Na začátku se pokusíme otevřít soubor. Pokud nastala chyba, zobrazíme chybovou zprávu a ukončíme funkci.
function CLoad3DS.Import3DS(var pModel: t3DModel; strFileName: string): boolean;// Nahraje model
var
strMessage: PAnsiChar;
currentChunk: tChunk;
begin
ZeroMemory(@currentChunk, sizeof(currentChunk));
m_FilePointer := FileOpen(strFileName, fmOpenRead);// Otevře 3DS soubor
if m_FilePointer = -1 then// Pokud nastala chyba, zobrazíme zprávu
begin
strMessage := PAnsiChar(Format('Unable to find the file: %s!', [strFileName]));
MessageBox(0, strMessage, 'Error', MB_OK);
Result := false;
exit;
end;
Pokud se nám podařilo soubor otevřít, načteme hlavičku prvního bloku. Podle identifikátoru bloku zjistíme, zda se jedná o primární blok. Pokud ne, pak se nejedná o 3ds soubor, a proto zobrazíme chybovou hlášku, uklidíme po sobě a ukončíme funkci.
ReadChunk(currentChunk);// Načte první blok
if currentChunk.ID <> PRIMARY then// Pokud máme jiný než primární blok, nejedná se o 3DS soubor
begin
strMessage := PAnsiChar(Format('Unable to load PRIMARY chunk from file: %s!', [strFileName]));
MessageBox(0, strMessage, 'Error', MB_OK);
CleanUp;
Result := false;
exit;
end;
Pomocí procedury ProcessNextChunk, která je volána rekurzivně (volá sama sebe), projdeme všechny bloky v souboru. Potom spočítáme normály a uklidíme.
ProcessNextChunk(pModel, currentChunk);// Načte objekty, procedura je volána rekurzivně
ComputeNormals(pModel);// Výpočet normál vrcholů
CleanUp;// Úklid
Result := true;
end;
Procedura ProcessNextChunk načítá jednotlivé bloky souboru.
procedure CLoad3DS.ProcessNextChunk(var pModel: t3DModel; var pPreviousChunk: tChunk);// Načte další bloky
var
currentChunk, tempChunk: tChunk;
begin
ZeroMemory(@currentChunk, sizeof(currentChunk));
ZeroMemory(@tempChunk, sizeof(tempChunk));
Nejdříve otestujeme, jestli už nejsme na konci bloku. Pokud ne, načteme hlavičku podbloku.
while pPreviousChunk.bytesRead < pPreviousChunk.length do// Cyklus dokud nenačteme celý blok
begin
ReadChunk(currentChunk);// Načte další podblok
Rozvětvíme kód podle jednotlivých identifikátorů bloků.
case currentChunk.ID of// Větvení podle hlavičky bloku
Blok s informací o verzi souboru. Verzi načteme do bufferu a zkontrolujeme, jestli není vyšší než 3. Pokud ano, zobrazíme varovnou hlášku o možné nekompatibilitě formátu.
VERSION:// Informace o verzi souboru
begin
currentChunk.bytesRead := currentChunk.bytesRead + FileRead(m_FilePointer, gBuffer, currentChunk.length - currentChunk.bytesRead);
if (currentChunk.length - currentChunk.bytesRead = 4) and (gBuffer[0] > $03) then// Pokud je verze vyšší než 3, zobrazíme varování
MessageBox(0, 'This 3DS file is over version 3 so it may load incorrectly', 'Warning', MB_OK);
end;
Blok OBJECTINFO je pouze kontejner, který obsahuje další bloky s informacemi o objektu. Hned proto načteme hlavičku podbloku a rekurzivně zavoláme proceduru ProcessNextChunk.
OBJECTINFO:// Hlavička objektu
begin
ReadChunk(tempChunk);// Načte další podblok
tempChunk.bytesRead := tempChunk.bytesRead + FileRead(m_FilePointer, gBuffer, tempChunk.length - tempChunk.bytesRead);
currentChunk.bytesRead := currentChunk.bytesRead + tempChunk.bytesRead;
ProcessNextChunk(pModel, currentChunk);// Načte další informace o objektu
end;
Blok MATERIAL je také pouze kontejner, uvozuje každý materiál použitý na objektu. Zvýšíme tedy počet materiálů a načteme další informace o materiálu.
MATERIAL:// Informace o materiálu
begin
Inc(pModel.numOfMaterials);// Zvýší počet materiálů
SetLength(pModel.pMaterials, Length(pModel.pMaterials) + 1);
ProcessNextMaterialChunk(pModel, currentChunk);// Načte další informace o materiálu
end;
Blok OBJEKT je opět jenom kontejner, uvozuje každý objekt použitý v modelu. Proto zvýšíme počet objektů, získáme jméno objektu a načteme zbývající informace o objektu.
OBJEKT:// Informace o objektu
begin
Inc(pModel.numOfObjects);// Zvýší počet objektů
SetLength(pModel.pObject, Length(pModel.pObject) + 1);
currentChunk.bytesRead := currentChunk.bytesRead + GetString(pModel.pObject[pModel.numOfObjects - 1].strName);// Načte jméno objektu
ProcessNextObjectChunk(pModel, pModel.pObject[pModel.numOfObjects - 1], currentChunk);// Načte zbývající informace o objektu
end;
Blok EDITKEYFRAME obsahuje informace o klíčových snímcích animace objektu. My ho nepoužíváme, proto zahodíme zbytek bloku do bufferu.
EDITKEYFRAME:// Klíčový snímek - nepoužito - přeskočíme
begin
currentChunk.bytesRead := currentChunk.bytesRead + FileRead(m_FilePointer, gBuffer, currentChunk.length - currentChunk.bytesRead);
end;
Pokud načteme neznámý blok, zahodíme jeho obsah do bufferu.
else// Pokud načteme neznámý blok, ignorujeme ho
begin
currentChunk.bytesRead := currentChunk.bytesRead + FileRead(m_FilePointer, gBuffer, currentChunk.length - currentChunk.bytesRead);
end;
end;
Nakonec zvětšíme počet přečtených bytů.
pPreviousChunk.bytesRead := pPreviousChunk.bytesRead + currentChunk.bytesRead;// Zvětšíme počet přečtených bytů
end;
end;
Procedura ProcessNextMaterialChunk načítá informace o materiálu. Na začátku zkontrolujeme, jestli nejsme na konci bloku. Pokud ne, načteme hlavičku podbloku a rozvětvíme kód podle identifikátoru hlavičky.
procedure CLoad3DS.ProcessNextMaterialChunk(var pModel: t3DModel; var pPreviousChunk: tChunk);// Načte informace o materiálu
var
currentChunk: tChunk;
begin
ZeroMemory(@currentChunk, sizeof(currentChunk));
while pPreviousChunk.bytesRead < pPreviousChunk.length do// Čte dokud nejsme na konci podbloku
begin
ReadChunk(currentChunk);// Načte blok
case currentChunk.ID of// Větvení podle hlavičky bloku
Blok MATNAME obsahuje jméno materiálu. Zavoláme funkci GetString, která ho načte.
MATNAME:// Načte jméno materiálu
currentChunk.bytesRead := currentChunk.bytesRead + GetString(pModel.pMaterials[pModel.numOfMaterials - 1].strName;
Blok MATDIFFUSE obsahuje informace o barvě objektu. Načteme je samostatnou funkcí.
MATDIFFUSE:// Načte barvu objektu
ReadColorChunk(pModel.pMaterials[pModel.numOfMaterials - 1], currentChunk);
Blok MATMAP obsahuje informace o materiálu. Voláme rekurzivně.
MATMAP:// Načte informace o materiálu
ProcessNextMaterialChunk(pModel, currentChunk);
Blok MATMAPFILE obsahuje jméno souboru obsahující texturu, která je použita v materiálu. Jméno souboru získáme funkcí GetString.
MATMAPFILE:// Načte jméno souboru s texturou
currentChunk.bytesRead := currentChunk.bytesRead + GetString(pModel.pMaterials[pModel.numOfMaterials - 1].strFile);
Pokud načteme neznámý blok, zahodíme jeho obsah do bufferu.
else// Ignorujeme neznámé bloky
currentChunk.bytesRead := currentChunk.bytesRead + FileRead(m_FilePointer, gBuffer, currentChunk.length - currentChunk.bytesRead);
end;
Nakonec zvětšíme počet přečtených bytů.
pPreviousChunk.bytesRead := pPreviousChunk.bytesRead + currentChunk.bytesRead;// Zvětšíme počet přečtených bytů
end;
end;
Procedura ProcessNextObjectChunk načítá informace o objektu. Jako obvykle zkontrolujeme, jestli už nejsme na konci bloku. Pokud ne, načteme hlavičku podbloku a rozvětvíme program podle identifikátoru hlavičky.
procedure CLoad3DS.ProcessNextObjectChunk(var pModel: t3DModel; var pObject: t3DObject; var pPreviousChunk: tChunk);// Načte informace o objektu
var
currentChunk: tChunk;
begin
while pPreviousChunk.bytesRead < pPreviousChunk.length do// Čte do konce podbloku
begin
ZeroMemory(@currentChunk, sizeof(currentChunk));
ReadChunk(currentChunk);// Načte další blok
case currentChunk.ID of// Větvení podle hlavičky
Blok OBJECT_MESH obsahuje informace o objektu. Volána rekurzivně.
OBJECT_MESH:// Nový objekt
ProcessNextObjectChunk(pModel, pObject, currentChunk);// Načte jeho informace
Blok OBJECT_VERTICES obsahuje vertexy objektu. Načteme je funkcí ReadVertices.
OBJECT_VERTICES:// Načte vertexy objektu
ReadVertices(pObject, currentChunk);
Blok OBJECT_FACES obsahuje plošky objektu. Načteme je funkcí ReadVertexIndices.
OBJECT_FACES:// Načte plošky objektu
ReadVertexIndices(pObject, currentChunk);
Blok OBJECT_MATERIAL obsahuje jméno použitého materiálu. Načteme ho funkcí ReadObjectMaterial.
OBJECT_MATERIAL:// Načte jméno použitého materiálu
ReadObjectMaterial(pModel, pObject, currentChunk);
Blok OBJECT_UV obsahuje texturové koordináty. Načteme je funkcí ReadUVCoordinates.
OBJECT_UV:// Načte texturové koordináty objektu
ReadUVCoordinates(pObject,currentChunk);
Ignorujeme neznámé bloky a inkrementujeme čítač přečtených bytů.
else// Ignorujeme neznámé bloky
currentChunk.bytesRead := currentChunk.bytesRead + FileRead(m_FilePointer, gBuffer, currentChunk.length - currentChunk.bytesRead);
end;
pPreviousChunk.bytesRead := pPreviousChunk.bytesRead + currentChunk.bytesRead;// Zvětšíme počet přečtených bytů
end;
end;
Procedura ReadColorChunk načítá barvu objektu. Zjistíme informace o bloku a na základě délky bloku použijeme vhodnou proměnnou pro uložení hodnot RGB. Originální kód počítal jen s variantou, že barva je uložena jako 3x byte (3x 1 byte). Ale já, když jsem exportoval svůj model z programu Cinema 4D, jsem zjistil, že je barva ukládaná jako 3x single (3x 4 byty). Proto jsem implementoval rozvětvení.
procedure CLoad3DS.ReadColorChunk(var pMaterial: tMaterialInfo; var pChunk: tChunk);// Načte barvu objektu
var
tempChunk: tChunk;
begin
ZeroMemory(@tempChunk, sizeof(tempChunk));
ReadChunk(tempChunk);// Informace o bloku
pMaterial.bpc := tempChunk.length - tempChunk.bytesRead;
if pMaterial.bpc = 3 then// Podle délky bloku načteme barvu do příslušného pole (3-byte, 12-single)
tempChunk.bytesRead := tempChunk.bytesRead + FileRead(m_FilePointer, pMaterial.colorub, pMaterial.bpc)
else
tempChunk.bytesRead := tempChunk.bytesRead + FileRead(m_FilePointer, pMaterial.colorf, pMaterial.bpc);
pChunk.bytesRead := pChunk.bytesRead + tempChunk.bytesRead;// Zvětšíme počet přečtených bytů
end;
Procedura ReadChunk načte hlavičku dalšího bloku.
procedure CLoad3DS.ReadChunk(var pChunk: tChunk);// Načte hlavičku bloku
begin
pChunk.bytesRead := FileRead(m_FilePointer, pChunk.ID, 2);// ID bloku
pChunk.bytesRead := pChunk.bytesRead + FileRead(m_FilePointer, pChunk.length, 4);// Délka bloku
end;
Procedura ReadObjectMaterial načte jméno použitého materiálu. Potom v cyklu nastavíme u objektu identifikátor použitého materiálu a pokud materiál obsahuje texturu, nastavíme příznak textury na TRUE.
procedure CLoad3DS.ReadObjectMaterial(pModel: t3DModel; var pObject: t3DObject; var pPreviousChunk: tChunk);// Načte materiály
var
strMaterial: string;
i: integer;
begin
strMaterial := '';
pPreviousChunk.bytesRead := pPreviousChunk.bytesRead + GetString(strMaterial);// Jméno materiálu
for i := 0 to pModel.numOfMaterials - 1 do// Projde všechny materiály
if strMaterial = pModel.pMaterials[i].strName then// Našli jsme náš materiál?
begin
pObject.materialID := i;// Nastavíme jeho index
if pModel.pMaterials[i].strFile <> '' then// Pokud existuje soubor s texturou
pObject.bHasTexture := true;// Nastavíme příznak
break;
end
else
pObject.materialID := -1;// Objekt nemá materiál
pPreviousChunk.bytesRead := pPreviousChunk.bytesRead + FileRead(m_FilePointer, Buffer, pPreviousChunk.length - pPreviousChunk.bytesRead);// Zvětšíme počet přečtených bytů
end;
Procedura ReadUVCoordinates načítá texturové koordináty. Nejdříve zjistíme počet koordinátů a nastavíme délku pole pro koordináty. V cyklu potom načteme všechny koordináty.
procedure CLoad3DS.ReadUVCoordinates(var pObject: t3DObject; var pPreviousChunk: tChunk);// Načte texturové koordináty
var
i: integer;
begin
pPreviousChunk.bytesRead := pPreviousChunk.bytesRead + FileRead(m_FilePointer, pObject.numTexVertex, 2);// Načte počet koordinátů
SetLength(pObject.pTexVerts,pObject.numTexVertex);// Nastaví délku pole
for i := 0 to pObject.numTexVertex - 1 do// Načte všechny koordináty
pPreviousChunk.bytesRead := pPreviousChunk.bytesRead + FileRead(m_FilePointer, pObject.pTexVerts[i], 8);
end;
Procedura ReadVertexIndices načítá indexy do pole vertexů. Na začátku zjistíme počet plošek a nastavíme pro ně velikost pole. V cyklech projdeme všechny plošky a jejich vrcholy a načteme indexy příslušných vertexů. U plošek nás zajímají jen první tři hodnoty představující indexy vrcholů, čtvrtá hodnota - viditelnost - je pro nás nezajímavá a tak ji přeskočíme.
procedure CLoad3DS.ReadVertexIndices(var pObject: t3DObject; var pPreviousChunk: tChunk);// Načte indexy do pole vertexů
var
index: word;
i, j: integer;
begin
pPreviousChunk.bytesRead := pPreviousChunk.bytesRead + FileRead(m_FilePointer, pObject.numOfFaces, 2);// Načte počet plošek
SetLength(pObject.pFaces, pObject.numOfFaces);// Nastaví velikost pole
ZeroMemory(pObject.pFaces, pObject.numOfFaces);
for i := 0 to pObject.numOfFaces - 1 do// Cyklus přes všechny plošky
for j := 0 to 3 do// Cyklus přes vrcholy plošek
begin
pPreviousChunk.bytesRead := pPreviousChunk.bytesRead + FileRead(m_FilePointer, index, sizeof(index));// Načte index vrcholu
if j < 3 then
pObject.pFaces[i].vertIndex[j] := index;// Uloží index do pole
end;
end;
Procedura ReadVertices čte jednotlivé vertexy. Zjistíme počet vertexů a nastavíme velikost pole. V cyklu načteme všechny vertexy a prohodíme jejich osy Y a Z, protože 3D Studio Max používá jiný systém os než OpenGL. Navíc ze stejného důvodu změníme orientaci u osy Z.
procedure CLoad3DS.ReadVertices(var pObject: t3DObject; var pPreviousChunk: tChunk);// Načte vertexy
var
i: integer;
fTempY: Double;
begin
pPreviousChunk.bytesRead := pPreviousChunk.bytesRead + FileRead(m_FilePointer, pObject.numOfVerts, 2);// Načte počet vertexů
SetLength(pObject.pVerts, pObject.numOfVerts);// Nastaví velikost pole
ZeroMemory(pObject.pVerts, pObject.numOfVerts);
for i := 0 to pObject.numOfVerts - 1 do// Cyklus přes všechny vertexy
begin
pPreviousChunk.bytesRead := pPreviousChunk.bytesRead + FileRead(m_FilePointer, pObject.pVerts[i], 12);
fTempY := pObject.pVerts[i].y;// Prohodí Y a Z
pObject.pVerts[i].y := pObject.pVerts[i].z;
pObject.pVerts[i].z := -fTempY;// Ještě změna orientace osy
end;
end;
end.
Tak, to bylo srdce celého načítání 3ds modelu. Nyní už jen upravíme NeHe kód aplikace a máme hotovo. Vysvětlím jen doplnění základního NeHe kódu, pro popis jeho kompletní struktury odkazuji na NeHe tutoriály. Na začátku přidáme jednotku glaux pro podporu nahrávání bitmapových obrázků a naši jednotku f_3ds, která načte 3ds model. Dále definujeme proměnné. g_Texture představuje pole použitých textur, g_Load3ds je naše třída 3ds a g_3DModel je proměnná, do které se načte 3ds model. g_ViewMode obsahuje způsob vykreslování vertexů (dále bude implementováno přepínání mezi klasickým a drátěným modelem) a následují proměnné pro zapnutí/vypnutí osvětlení a pro rotaci objektu. Nakonec zvolíme jméno nahrávaného souboru.
g_Texture: array of UINT;// Pole textur
g_Load3ds: CLoad3DS;// Třída 3DS
g_3DModel: t3DModel;// 3DS model
g_ViewMode: integer = GL_TRIANGLES;// Způsob vykreslování
g_bLighting: boolean = true;// Osvětlení
g_RotateX: GLfloat = 0.0;// Rotace
g_RotationSpeed: GLfloat = 0.8;// Rychlost rotace
// Vždy nechte odkomentovaný pouze jeden řádek, ostatní zakomentujte!!!
FILE_NAME: string = 'face.3ds';// Soubor, který budeme nahrávat - originál
//FILE_NAME: string = 'muz.3ds';// Předpřipravený objekt v Cinema 4D
//FILE_NAME: string = 'snehulak.3ds';// Můj výtvor
Vytvoříme proceduru CreateTexture, která se bude starat o nahrávání textur. Jako parametry jí předáme pole textur, jméno souboru s obrázkem a identifikátor textury (index do pole textur). Následuje standardní kód na vytvoření mipmapované textury z bitmapového obrázku. Tedy nejdříve načteme obrázek do pomocné proměnné, zavoláme funkci glGenTextures a necháme si vygenerovat jednu texturu. Nastavíme způsob interpretace načtených dat - funkce glPixelStorei, zvolíme vytvořenou texturu - glBindTexture, převedeme obrázek na mipmapovou texturu - gluBuild2DMipmaps a nakonec nastavíme její parametry - glTexParameteri.
procedure CreateTexture(var textureArray: array of UINT; strFileName: LPSTR; textureID: integer);// Vytvoření textury
var
pBitmap: PTAUX_RGBImageRec;// Pomocná proměnná pro nahrání bitmapy
begin
if strFileName = '' then exit;// Bylo předáno jméno soubor?
pBitmap := auxDIBImageLoadA(strFileName);// Načtení bitmapy
if pBitmap = nil then exit;// Podařilo se načíst bitmapu?
glGenTextures(1, textureArray[textureID]);// Generujeme 1 texturu
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);// Způsob interpretace dat v paměti (1 - hodnoty typu byte)
glBindTexture(GL_TEXTURE_2D, textureArray[textureID]);// Zvolí texturu
gluBuild2DMipmaps(GL_TEXTURE_2D, 3, pBitmap.sizeX, pBitmap.sizeY, GL_RGB, GL_UNSIGNED_BYTE, pBitmap.data);// Vytvoří mipmapovou texturu
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);// Způsob filtrování
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_LINEAR);
end;
Do funkce Initialize přidáme následující kód. Nejprve otestujeme, zda při spuštění programu nebyl náhodou předán parametr (V C/C++ se jedná o parametry argc, argv funkce main().). Jako jediný možný parametr je jméno souboru s modelem. Pokud bylo předáno, má přednost před jménem, které je nastaveno v programu "natvrdo". Rozhodl jsem se to implementovat pro ty, kteří pro otestování programu nechtějí (nebo nemůžou - neprogramátoři) spustit vývojové prostředí a chtějí zkusit nahrát jiný model. Pak stačí napsat: main.exe muz.3ds a místo standardního modelu face.3ds se nahraje model muže.
Jen bych chtěl dopředu upozornit, že mé modely byly velikostně upraveny tak, aby se nemuselo hýbat s viewportem. Pokud se rozhodnete načíst svůj vlastní model, pak je velmi pravděpodobné, že budete muset změnit parametry funkce gluLookAt, která se nachází v proceduře Draw a nastavuje pohled kamery!!! Pro "jednoduchost" tohoto ukázkového kódu jsem místo změny pohledu kamery raději zvolil změnu velikosti vytvářeného objektu.
// Funkce Initialize
if ParamCount <> 0 then FILE_NAME := ParamStr(1);// Pokud byl programu předán soubor jako parametr, pak jej načteme místo souboru, který je definovám přímo v programu
Dále vytvoříme instanci třídy a nahrajeme model ze souboru. Podle počtu materiálů nastavíme velikost pole pro jejich textury. V cyklu projdeme všechny materiály a pokud obsahují textury, tak je vytvoříme. Nakonec zapneme světlo nula, osvětlení, barvu materiálu, mapování textur a testování hloubky.
ZeroMemory(@g_3DModel, sizeof(g_3DModel));// Nulování paměti
g_Load3ds := CLoad3DS.Create;// Vytvoří instanci třídy 3DS
g_Load3ds.Import3DS(g_3DModel, FILE_NAME);// Nahraje model
SetLength(g_Texture, g_3DModel.numOfMaterials);// Nastaví velikost pole pro textury
for i := 0 to g_3DModel.numOfMaterials -1 do// Cyklus přes jednotlivé materiály
begin
if g_3DModel.pMaterials[i].strFile <> '' then// Obsahuje materiál texturu?
CreateTexture(g_Texture, PChar(g_3DModel.pMaterials[i].strFile), i);// Pokud ano, pak ji vytvoříme
g_3DModel.pMaterials[i].texureId := i;// Uloží ID textury
end;
glEnable(GL_LIGHT0);// Zapne světlo
glEnable(GL_LIGHTING);// Zapne osvětlení
glEnable(GL_COLOR_MATERIAL);// Povolí barvu materiálu
glEnable(GL_TEXTURE_2D);// Povolí mapování textur
glEnable(GL_DEPTH_TEST);// Zapne testování hloubky
V proceduře Deinitialize se postaráme o uvolnění alokovaných prostředků. V objektu nastavíme délku všech dynamických polí na 0 - tím uvolníme paměť, kterou zabíraly. Uvolníme paměť pro textury a zrušíme instanci třídy 3ds.
// Funkce Deinitialize
for i := 0 to g_3DModel.numOfObjects - 1 do// Projde všechny objekty v modelu
begin
SetLength(g_3DModel.pObject[i].pVerts, 0);// Uvolní pole vertexů
SetLength(g_3DModel.pObject[i].pNormals, 0);// Uvolní pole normál
SetLength(g_3DModel.pObject[i].pTexVerts, 0);// Uvolní pole texturovaných vertexů
SetLength(g_3DModel.pObject[i].pFaces, 0);// Uvolní pole faců
end;
glDeleteTextures(g_3DModel.numOfMaterials, @g_Texture);// Uvolní paměť pro textury
SetLength(g_Texture, 0);// Uvolní pole textur
g_Load3ds.Free;// Zruší instanci třídy 3DS
Do procedury Update doplníme obsluhu stisku kláves. Šipkami budeme ovlivňovat rotaci objektu.
// Funkce Update
if g_keys.keyDown[VK_LEFT] then// Šipka vlevo
g_RotationSpeed := g_RotationSpeed - 0.05;// Úprava rotace
if g_keys.keyDown[VK_RIGHT] then// Šipka vpravo
g_RotationSpeed := g_RotationSpeed + 0.05;// Úprava rotace
Veškeré vykreslování se provádí v proceduře Draw. Ne jejím začátku smažeme obrazovku a hloubkový buffer a resetujeme matici.
procedure Draw;// Vykreslení scény
var
i, j, whichVertex: integer;// Cykly
pObject: t3DObject;// Objekt
index: integer;// Index do pole vertexů
begin
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);// Smaže obrazovku a hloubkový buffer
glLoadIdentity;// Reset matice
Dále nastavíme pohled kamery, rotaci modelu a aktualizujeme úhel natočení modelu.
gluLookAt(0,1.5,8, 0,0.5,0, 0,1,0);// Nastavení pohledu kamery
glRotatef(g_RotateX, 0, 1.0, 0);// Rotace modelu
g_RotateX := g_RotateX + g_RotationSpeed;// Aktualizace úhlu natočení
Vykreslíme všechny objekty modelu. Na začátku cyklu testujeme, zda model obsahuje objekty a pokud ne, tak cyklus ukončíme. Objekt uložíme do pomocné proměnné a otestujeme, jestli má texturu. Pokud ano, zapneme mapování textur a příslušnou texturu zvolíme. Pokud ne, vypneme mapování textur.
for i := 0 to g_3DModel.numOfObjects - 1 do// Projde všechny objekty v modelu
begin
if Length(g_3DModel.pObject) = 0 then break;// Pokud model neobsahuje žádný objekt, ukončíme cyklus
pObject := g_3DModel.pObject[i];// Uložíme objekt do pomocné proměnné
if pObject.bHasTexture then// Má objekt texturu?
begin// Pokud ano
glEnable(GL_TEXTURE_2D);// Zapne mapování textur
glColor3ub(255, 255, 255);
glBindTexture(GL_TEXTURE_2D, g_Texture[pObject.materialID]);// Zvolí příslušnou texturu
end
else// Pokud ne
begin
glDisable(GL_TEXTURE_2D);// Vypne mapování textur
glColor3ub(255, 255, 255);
end;
Projdeme všechny trojúhelníky, nastavíme normály a pokud má objekt texturu nastavíme i texturové koordináty. Pokud objekt texturu nemá, nastavíme jeho barvu. Nakonec vykreslíme vlastní vertexy.
glBegin(g_ViewMode);// Začátek vykreslování vertexů
for j := 0 to pObject.numOfFaces - 1 do// Projde všechny trojúhelníky
for whichVertex := 0 to 2 do// Projde všechny vrcholy trojúhelníků
begin
index := pObject.pFaces[j].vertIndex[whichVertex];// Index do pole vertexů
glNormal3f(pObject.pNormals[index].x, pObject.pNormals[index].y, pObject.pNormals[index].z);// Nastaví normálu
if pObject.bHasTexture then// Má objekt texturu?
begin
if Assigned(pObject.pTexVerts) then// Má objekt texturové koordináty?
glTexCoord2f(pObject.pTexVerts[index].x, pObject.pTexVerts[index].y);// Nastaví texturové koordináty
end
else
begin
if (Length(g_3DModel.pMaterials) <> 0) and (pObject.materialID >= 0) then// Když objekt nemá texturu má alespoň materiál?
if g_3DModel.pMaterials[pObject.materialID].bpc = 3 then// Podle počtu bytů na barevný kanál, použijeme vhodnou funkci pro nastavení barvy
glColor3ubv(@g_3DModel.pMaterials[pObject.materialID].colorub)
else
glColor3fv(@g_3DModel.pMaterials[pObject.materialID].colorf);
end;
glVertex3f(pObject.pVerts[index].x, pObject.pVerts[index].y, pObject.pVerts[index].z);// Nakonec vykreslíme vertex
end;
glEnd;// Konec vykreslování vertexů
end;
glFlush;// Vyprázdní OpenGL renderovací pipeline
end;
Do funkce WindowProc přidáme podporu myši. Při stisku levého tlačítka myši dojde ke změně módu vykreslování na drátěný model a zpět. Stiskem pravého tlačítka myši se zapíná/vypíná osvětlení.
// WindowProc
WM_LBUTTONDOWN:// Obsluha levého tlačítka myši
begin
if g_ViewMode = GL_TRIANGLES then
g_ViewMode := GL_LINE_STRIP// Změna módu vykreslování
else
g_ViewMode := GL_TRIANGLES;
Result := 0;
end;
WM_RBUTTONDOWN:// Obsluha pravého tlačítka myši
begin
g_bLighting := not g_bLighting;// Zapne/vypne osvětlení
if g_bLighting then
glEnable(GL_LIGHTING)
else
glDisable(GL_LIGHTING);
Result := 0;
end;
Tak a máme hotovo. Teď už jen zbývá náš výtvor spustit. Po několika probdělých nocích s hexavýpisem souboru a kalkulačkou v ruce a dalších volných chvílích dne, jsem se konečně dobral výsledku. Uff!!! :-)))
napsal: Michal Tuček <michal_praha (zavináč) seznam.cz>, 23.08.2004
C++ předloha zdrojového kódu pro článek: http://www.gametutorials.com/ (Pozn.: Některé tutoriály na gametutorials.com začaly být placené!)