Načítání .3DS modelů

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ů
Bod 1 náleží jen jedné plošce - normálový vektor vertexu bude shodný s vektorem plošky.
Bod 2 náleží ke dvěma ploškám - jeho normálový vektor bude složen z vektorů obou plošek.
Bod 3 náleží ke třem ploškám - jeho normálový vektor bude složen z vektorů všech tří plošek.

// 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é!)

Zdrojové kódy

Model tváře
Model sněhuláka
Model muže