mirror of
https://github.com/dhewm/dhewm3.git
synced 2024-11-30 08:01:02 +00:00
1641 lines
43 KiB
C++
1641 lines
43 KiB
C++
/*
|
|
===========================================================================
|
|
|
|
Doom 3 GPL Source Code
|
|
Copyright (C) 1999-2011 id Software LLC, a ZeniMax Media company.
|
|
|
|
This file is part of the Doom 3 GPL Source Code ("Doom 3 Source Code").
|
|
|
|
Doom 3 Source Code is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Doom 3 Source Code is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Doom 3 Source Code. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
In addition, the Doom 3 Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 Source Code. If not, please request a copy in writing from id Software at the address below.
|
|
|
|
If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.
|
|
|
|
===========================================================================
|
|
*/
|
|
|
|
#include "sys/platform.h"
|
|
#include "renderer/ModelManager.h"
|
|
#include "renderer/tr_local.h"
|
|
|
|
#include "tools/compilers/compiler_public.h"
|
|
|
|
#ifdef WIN32
|
|
#include <windows.h>
|
|
#include <GL/gl.h>
|
|
#include <GL/glu.h>
|
|
#include "sys/win32/win_local.h"
|
|
#endif
|
|
|
|
/*
|
|
|
|
render a normalmap tga file from an ase model for bump mapping
|
|
|
|
To make ray-tracing into the high poly mesh efficient, we preconstruct
|
|
a 3D hash table of the triangles that need to be tested for a given source
|
|
point.
|
|
|
|
This task is easier than a general ray tracing optimization, because we
|
|
known that all of the triangles are going to be "near" the source point.
|
|
|
|
TraceFraction determines the maximum distance in any direction that
|
|
a trace will go. It is expressed as a fraction of the largest axis of
|
|
the bounding box, so it doesn't matter what units are used for modeling.
|
|
|
|
|
|
*/
|
|
|
|
#define MAX_QPATH 256
|
|
|
|
#define DEFAULT_TRACE_FRACTION 0.05
|
|
|
|
#define INITIAL_TRI_TO_LINK_EXPANSION 16 // can grow as needed
|
|
#define HASH_AXIS_BINS 100
|
|
|
|
typedef struct {
|
|
int faceNum;
|
|
int nextLink;
|
|
} triLink_t;
|
|
|
|
typedef struct {
|
|
int triLink;
|
|
int rayNumber; // don't need to test again if still on same ray
|
|
} binLink_t;
|
|
|
|
#define MAX_LINKS_PER_BLOCK 0x100000
|
|
#define MAX_LINK_BLOCKS 0x100
|
|
typedef struct {
|
|
idBounds bounds;
|
|
float binSize[3];
|
|
int numLinkBlocks;
|
|
triLink_t *linkBlocks[MAX_LINK_BLOCKS];
|
|
binLink_t binLinks[HASH_AXIS_BINS][HASH_AXIS_BINS][HASH_AXIS_BINS];
|
|
} triHash_t;
|
|
|
|
typedef struct {
|
|
char outputName[MAX_QPATH];
|
|
char highName[MAX_QPATH];
|
|
byte *localPic;
|
|
byte *globalPic;
|
|
byte *colorPic;
|
|
float *edgeDistances; // starts out -1 for untraced, for each texel, 0 = true interior, >0 = off-edge rasterization
|
|
int width, height;
|
|
int antiAlias;
|
|
int outline;
|
|
bool saveGlobalMap;
|
|
bool saveColorMap;
|
|
float traceFrac;
|
|
float traceDist;
|
|
srfTriangles_t *mesh; // high poly mesh
|
|
idRenderModel *highModel;
|
|
triHash_t *hash;
|
|
} renderBump_t;
|
|
|
|
static int rayNumber; // for avoiding retests of bins and faces
|
|
|
|
static int oldWidth, oldHeight;
|
|
|
|
/*
|
|
===============
|
|
SaveWindow
|
|
===============
|
|
*/
|
|
static void SaveWindow( void ) {
|
|
oldWidth = glConfig.vidWidth;
|
|
oldHeight = glConfig.vidHeight;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
ResizeWindow
|
|
===============
|
|
*/
|
|
static void ResizeWindow( int width, int height ) {
|
|
#if defined(WIN32) && defined(ID_ALLOW_TOOLS)
|
|
int winWidth, winHeight;
|
|
if ( glConfig.isFullscreen ) {
|
|
winWidth = width;
|
|
winHeight = height;
|
|
} else {
|
|
RECT r;
|
|
|
|
// adjust width and height for window border
|
|
r.bottom = height;
|
|
r.left = 0;
|
|
r.top = 0;
|
|
r.right = width;
|
|
|
|
AdjustWindowRect (&r, WINDOW_STYLE|WS_SYSMENU, FALSE);
|
|
winHeight = r.bottom - r.top;
|
|
winWidth = r.right - r.left;
|
|
|
|
}
|
|
SetWindowPos( win32.hWnd, HWND_TOP, 0, 0, winWidth, winHeight, SWP_SHOWWINDOW );
|
|
|
|
// FIXME: ??? qwglMakeCurrent( win32.hDC, win32.hGLRC );
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
===============
|
|
RestoreWindow
|
|
===============
|
|
*/
|
|
static void RestoreWindow( void ) {
|
|
#if defined(WIN32) && defined(ID_ALLOW_TOOLS)
|
|
int winWidth, winHeight;
|
|
if ( glConfig.isFullscreen ) {
|
|
winWidth = oldWidth;
|
|
winHeight = oldHeight;
|
|
} else {
|
|
RECT r;
|
|
|
|
// adjust width and height for window border
|
|
r.bottom = oldHeight;
|
|
r.left = 0;
|
|
r.top = 0;
|
|
r.right = oldWidth;
|
|
|
|
AdjustWindowRect (&r, WINDOW_STYLE|WS_SYSMENU, FALSE);
|
|
winHeight = r.bottom - r.top;
|
|
winWidth = r.right - r.left;
|
|
}
|
|
SetWindowPos( win32.hWnd, HWND_TOP, 0, 0, winWidth, winHeight, SWP_SHOWWINDOW );
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
================
|
|
OutlineNormalMap
|
|
|
|
Puts a single pixel border around all non-empty pixels
|
|
Does NOT copy the alpha channel, so it can be used as
|
|
an alpha test map.
|
|
================
|
|
*/
|
|
static void OutlineNormalMap( byte *data, int width, int height, int emptyR, int emptyG, int emptyB ) {
|
|
byte *orig;
|
|
int i, j, k, l;
|
|
idVec3 normal;
|
|
byte *out;
|
|
|
|
orig = (byte *)Mem_Alloc( width * height * 4 );
|
|
memcpy( orig, data, width * height * 4 );
|
|
|
|
for ( i = 0 ; i < width ; i++ ) {
|
|
for ( j = 0 ; j < height ; j++ ) {
|
|
out = data + ( j * width + i ) * 4;
|
|
if ( out[0] != emptyR || out[1] != emptyG || out[2] != emptyB ) {
|
|
continue;
|
|
}
|
|
|
|
normal = vec3_origin;
|
|
for ( k = -1 ; k < 2 ; k++ ) {
|
|
for ( l = -1 ; l < 2 ; l++ ) {
|
|
byte *in;
|
|
|
|
in = orig + ( ((j+l)&(height-1))*width + ((i+k)&(width-1)) ) * 4;
|
|
|
|
if ( in[0] == emptyR && in[1] == emptyG && in[2] == emptyB ) {
|
|
continue;
|
|
}
|
|
|
|
normal[0] += ( in[0] - 128 );
|
|
normal[1] += ( in[1] - 128 );
|
|
normal[2] += ( in[2] - 128 );
|
|
}
|
|
}
|
|
|
|
if ( normal.Normalize() < 0.5 ) {
|
|
continue; // no valid samples
|
|
}
|
|
|
|
out[0] = 128 + 127 * normal[0];
|
|
out[1] = 128 + 127 * normal[1];
|
|
out[2] = 128 + 127 * normal[2];
|
|
}
|
|
}
|
|
|
|
Mem_Free( orig );
|
|
}
|
|
|
|
/*
|
|
================
|
|
OutlineColorMap
|
|
|
|
Puts a single pixel border around all non-empty pixels
|
|
Does NOT copy the alpha channel, so it can be used as
|
|
an alpha test map.
|
|
================
|
|
*/
|
|
static void OutlineColorMap( byte *data, int width, int height, int emptyR, int emptyG, int emptyB ) {
|
|
byte *orig;
|
|
int i, j, k, l;
|
|
idVec3 normal;
|
|
byte *out;
|
|
|
|
orig = (byte *)Mem_Alloc( width * height * 4 );
|
|
memcpy( orig, data, width * height * 4 );
|
|
|
|
for ( i = 0 ; i < width ; i++ ) {
|
|
for ( j = 0 ; j < height ; j++ ) {
|
|
out = data + ( j * width + i ) * 4;
|
|
if ( out[0] != emptyR || out[1] != emptyG || out[2] != emptyB ) {
|
|
continue;
|
|
}
|
|
|
|
normal = vec3_origin;
|
|
int count = 0;
|
|
for ( k = -1 ; k < 2 ; k++ ) {
|
|
for ( l = -1 ; l < 2 ; l++ ) {
|
|
byte *in;
|
|
|
|
in = orig + ( ((j+l)&(height-1))*width + ((i+k)&(width-1)) ) * 4;
|
|
|
|
if ( in[0] == emptyR && in[1] == emptyG && in[2] == emptyB ) {
|
|
continue;
|
|
}
|
|
|
|
normal[0] += in[0];
|
|
normal[1] += in[1];
|
|
normal[2] += in[2];
|
|
count++;
|
|
}
|
|
}
|
|
|
|
if ( !count ) {
|
|
continue;
|
|
}
|
|
normal *= (1.0 / count );
|
|
|
|
out[0] = normal[0];
|
|
out[1] = normal[1];
|
|
out[2] = normal[2];
|
|
}
|
|
}
|
|
|
|
Mem_Free( orig );
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
================
|
|
FreeTriHash
|
|
================
|
|
*/
|
|
static void FreeTriHash( triHash_t *hash ) {
|
|
for ( int i = 0 ; i < hash->numLinkBlocks ; i++ ) {
|
|
Mem_Free( hash->linkBlocks[i] );
|
|
}
|
|
Mem_Free( hash );
|
|
}
|
|
|
|
/*
|
|
================
|
|
CreateTriHash
|
|
================
|
|
*/
|
|
static triHash_t *CreateTriHash( const srfTriangles_t *highMesh ) {
|
|
triHash_t *hash;
|
|
int i, j, k, l;
|
|
idBounds bounds, triBounds;
|
|
int iBounds[2][3];
|
|
int maxLinks, numLinks;
|
|
|
|
hash = (triHash_t *)Mem_Alloc( sizeof( *hash ) );
|
|
memset( hash, 0, sizeof( *hash ) );
|
|
|
|
// find the bounding volume for the mesh
|
|
bounds.Clear();
|
|
for ( i = 0 ; i < highMesh->numVerts ; i++ ) {
|
|
bounds.AddPoint( highMesh->verts[i].xyz );
|
|
}
|
|
|
|
hash->bounds = bounds;
|
|
|
|
// divide each axis as needed
|
|
for ( i = 0 ; i < 3 ; i++ ) {
|
|
hash->binSize[i] = ( bounds[1][i] - bounds[0][i] ) / HASH_AXIS_BINS;
|
|
if ( hash->binSize[i] <= 0 ) {
|
|
common->FatalError( "CreateTriHash: bad bounds: (%f %f %f) to (%f %f %f)",
|
|
bounds[0][0],bounds[0][1],bounds[0][2],
|
|
bounds[1][0],bounds[1][1],bounds[1][2] );
|
|
}
|
|
}
|
|
|
|
// a -1 link number terminated the link chain
|
|
memset( hash->binLinks, -1, sizeof( hash->binLinks ) );
|
|
|
|
numLinks = 0;
|
|
|
|
hash->linkBlocks[hash->numLinkBlocks] = (triLink_t *)Mem_Alloc( MAX_LINKS_PER_BLOCK * sizeof( triLink_t ) );
|
|
hash->numLinkBlocks++;
|
|
maxLinks = hash->numLinkBlocks * MAX_LINKS_PER_BLOCK;
|
|
|
|
// for each triangle, place a triLink in each bin that might reference it
|
|
for ( i = 0 ; i < highMesh->numIndexes ; i+=3 ) {
|
|
// determine which hash bins the triangle will need to be in
|
|
triBounds.Clear();
|
|
for ( j = 0 ; j < 3 ; j++ ) {
|
|
triBounds.AddPoint( highMesh->verts[ highMesh->indexes[i+j] ].xyz );
|
|
}
|
|
for ( j = 0 ; j < 3 ; j++ ) {
|
|
iBounds[0][j] = ( triBounds[0][j] - hash->bounds[0][j] ) / hash->binSize[j];
|
|
iBounds[0][j] -= 0.001; // epsilon
|
|
if ( iBounds[0][j] < 0 ) {
|
|
iBounds[0][j] = 0;
|
|
} else if ( iBounds[0][j] >= HASH_AXIS_BINS ) {
|
|
iBounds[0][j] = HASH_AXIS_BINS-1;
|
|
}
|
|
|
|
iBounds[1][j] = ( triBounds[1][j] - hash->bounds[0][j] ) / hash->binSize[j];
|
|
iBounds[0][j] += 0.001; // epsilon
|
|
if ( iBounds[1][j] < 0 ) {
|
|
iBounds[1][j] = 0;
|
|
} else if ( iBounds[1][j] >= HASH_AXIS_BINS ) {
|
|
iBounds[1][j] = HASH_AXIS_BINS-1;
|
|
}
|
|
}
|
|
|
|
// add the links
|
|
for ( j = iBounds[0][0] ; j <= iBounds[1][0] ; j++ ) {
|
|
for ( k = iBounds[0][1] ; k <= iBounds[1][1] ; k++ ) {
|
|
for ( l = iBounds[0][2] ; l <= iBounds[1][2] ; l++ ) {
|
|
if ( numLinks == maxLinks ) {
|
|
hash->linkBlocks[hash->numLinkBlocks] = (triLink_t *)Mem_Alloc( MAX_LINKS_PER_BLOCK * sizeof( triLink_t ) );
|
|
hash->numLinkBlocks++;
|
|
maxLinks = hash->numLinkBlocks * MAX_LINKS_PER_BLOCK;
|
|
}
|
|
|
|
triLink_t *link = &hash->linkBlocks[ numLinks / MAX_LINKS_PER_BLOCK ][ numLinks % MAX_LINKS_PER_BLOCK ];
|
|
link->faceNum = i / 3;
|
|
link->nextLink = hash->binLinks[j][k][l].triLink;
|
|
hash->binLinks[j][k][l].triLink = numLinks;
|
|
numLinks++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
common->Printf( "%i triangles made %i links\n", highMesh->numIndexes / 3, numLinks );
|
|
|
|
return hash;
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
TraceToMeshFace
|
|
|
|
Returns the distance from the point to the intersection, or DIST_NO_INTERSECTION
|
|
=================
|
|
*/
|
|
#define DIST_NO_INTERSECTION -999999999.0f
|
|
static float TraceToMeshFace( const srfTriangles_t *highMesh, int faceNum,
|
|
float minDist, float maxDist,
|
|
const idVec3 &point, const idVec3 &normal, idVec3 &sampledNormal,
|
|
byte sampledColor[4] ) {
|
|
int j;
|
|
float dist;
|
|
const idVec3 *v[3];
|
|
const idPlane *plane;
|
|
idVec3 edge;
|
|
float d;
|
|
idVec3 dir[3];
|
|
float baseArea;
|
|
float bary[3];
|
|
idVec3 testVert;
|
|
|
|
v[0] = &highMesh->verts[ highMesh->indexes[ faceNum * 3 + 0 ] ].xyz;
|
|
v[1] = &highMesh->verts[ highMesh->indexes[ faceNum * 3 + 1 ] ].xyz;
|
|
v[2] = &highMesh->verts[ highMesh->indexes[ faceNum * 3 + 2 ] ].xyz;
|
|
|
|
plane = highMesh->facePlanes + faceNum;
|
|
|
|
// only test against planes facing the same direction as our normal
|
|
d = plane->Normal() * normal;
|
|
if ( d <= 0.0001f ) {
|
|
return DIST_NO_INTERSECTION;
|
|
}
|
|
|
|
// find the point of impact on the plane
|
|
dist = plane->Distance( point );
|
|
dist /= -d;
|
|
|
|
testVert = point + dist * normal;
|
|
|
|
// if this would be beyond our requested trace distance,
|
|
// don't even check it
|
|
if ( dist > maxDist ) {
|
|
return DIST_NO_INTERSECTION;
|
|
}
|
|
|
|
if ( dist < minDist ) {
|
|
return DIST_NO_INTERSECTION;
|
|
}
|
|
|
|
// if normal is inside all edge planes, this face is hit
|
|
VectorSubtract( *v[0], point, dir[0] );
|
|
VectorSubtract( *v[1], point, dir[1] );
|
|
edge = dir[0].Cross( dir[1] );
|
|
d = DotProduct( normal, edge );
|
|
if ( d > 0.0f ) {
|
|
return DIST_NO_INTERSECTION;
|
|
}
|
|
VectorSubtract( *v[2], point, dir[2] );
|
|
edge = dir[1].Cross( dir[2] );
|
|
d = DotProduct( normal, edge );
|
|
if ( d > 0.0f ) {
|
|
return DIST_NO_INTERSECTION;
|
|
}
|
|
edge = dir[2].Cross( dir[0] );
|
|
d = DotProduct( normal, edge );
|
|
if ( d > 0.0f ) {
|
|
return DIST_NO_INTERSECTION;
|
|
}
|
|
|
|
// calculate barycentric coordinates of the impact point
|
|
// on the high poly triangle
|
|
bary[0] = idWinding::TriangleArea( testVert, *v[1], *v[2] );
|
|
bary[1] = idWinding::TriangleArea( *v[0], testVert, *v[2] );
|
|
bary[2] = idWinding::TriangleArea( *v[0], *v[1], testVert );
|
|
|
|
baseArea = idWinding::TriangleArea( *v[0], *v[1], *v[2] );
|
|
bary[0] /= baseArea;
|
|
bary[1] /= baseArea;
|
|
bary[2] /= baseArea;
|
|
|
|
if ( bary[0] + bary[1] + bary[2] > 1.1 ) {
|
|
bary[0] = bary[0];
|
|
return DIST_NO_INTERSECTION;
|
|
}
|
|
|
|
// triangularly interpolate the normals to the sample point
|
|
sampledNormal = vec3_origin;
|
|
for ( j = 0 ; j < 3 ; j++ ) {
|
|
sampledNormal += bary[j] * highMesh->verts[ highMesh->indexes[ faceNum * 3 + j ] ].normal;
|
|
}
|
|
sampledNormal.Normalize();
|
|
|
|
sampledColor[0] = sampledColor[1] = sampledColor[2] = sampledColor[3] = 0;
|
|
for ( int i = 0 ; i < 4 ; i++ ) {
|
|
float color = 0.0f;
|
|
for ( j = 0 ; j < 3 ; j++ ) {
|
|
color += bary[j] * highMesh->verts[ highMesh->indexes[ faceNum * 3 + j ] ].color[i];
|
|
}
|
|
sampledColor[i] = color;
|
|
}
|
|
return dist;
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
SampleHighMesh
|
|
|
|
Find the best surface normal in the high poly mesh
|
|
for a ray coming from the surface of the low poly mesh
|
|
|
|
Returns false if the trace doesn't hit anything
|
|
================
|
|
*/
|
|
static bool SampleHighMesh( const renderBump_t *rb,
|
|
const idVec3 &point, const idVec3 &direction, idVec3 &sampledNormal,
|
|
byte sampledColor[4] ) {
|
|
idVec3 p;
|
|
binLink_t *bl;
|
|
int linkNum;
|
|
int faceNum;
|
|
float dist, bestDist;
|
|
int block[3];
|
|
float maxDist;
|
|
int c_hits;
|
|
int i;
|
|
idVec3 normal;
|
|
|
|
// we allow non-normalized directions on input
|
|
normal = direction;
|
|
normal.Normalize();
|
|
|
|
// increment our uniqueness counter (FIXME: make thread safe?)
|
|
rayNumber++;
|
|
|
|
// the max distance will be the traceFrac times the longest axis of the high poly model
|
|
bestDist = -rb->traceDist;
|
|
maxDist = rb->traceDist;
|
|
|
|
sampledNormal = vec3_origin;
|
|
|
|
c_hits = 0;
|
|
|
|
// this is a pretty damn lazy way to walk through a 3D grid, and has a (very slight)
|
|
// chance of missing a triangle in a corner crossing case
|
|
#define RAY_STEPS 100
|
|
for ( i = 0 ; i < RAY_STEPS ; i++ ) {
|
|
p = point - rb->hash->bounds[0] + normal * ( -1.0 + 2.0 * i / RAY_STEPS ) * rb->traceDist;
|
|
|
|
block[0] = floor( p[0] / rb->hash->binSize[0] );
|
|
block[1] = floor( p[1] / rb->hash->binSize[1] );
|
|
block[2] = floor( p[2] / rb->hash->binSize[2] );
|
|
|
|
if ( block[0] < 0 || block[0] >= HASH_AXIS_BINS ) {
|
|
continue;
|
|
}
|
|
if ( block[1] < 0 || block[1] >= HASH_AXIS_BINS ) {
|
|
continue;
|
|
}
|
|
if ( block[2] < 0 || block[2] >= HASH_AXIS_BINS ) {
|
|
continue;
|
|
}
|
|
|
|
// FIXME: casting away const
|
|
bl = (binLink_t *)&rb->hash->binLinks[block[0]][block[1]][block[2]];
|
|
if ( bl->rayNumber == rayNumber ) {
|
|
continue; // already tested this block
|
|
}
|
|
bl->rayNumber = rayNumber;
|
|
linkNum = bl->triLink;
|
|
triLink_t *link;
|
|
for ( ; linkNum != -1 ; linkNum = link->nextLink ) {
|
|
link = &rb->hash->linkBlocks[ linkNum / MAX_LINKS_PER_BLOCK ][ linkNum % MAX_LINKS_PER_BLOCK ];
|
|
|
|
faceNum = link->faceNum;
|
|
dist = TraceToMeshFace( rb->mesh, faceNum,
|
|
bestDist, maxDist, point, normal, sampledNormal, sampledColor );
|
|
if ( dist == DIST_NO_INTERSECTION ) {
|
|
continue;
|
|
}
|
|
|
|
c_hits++;
|
|
// continue looking for a better match
|
|
bestDist = dist;
|
|
}
|
|
}
|
|
|
|
return (bool)( bestDist > -rb->traceDist );
|
|
}
|
|
|
|
/*
|
|
=============
|
|
TriTextureArea
|
|
|
|
This may be negatove
|
|
=============
|
|
*/
|
|
static float TriTextureArea( const float a[2], const float b[2], const float c[2] ) {
|
|
idVec3 d1, d2;
|
|
idVec3 cross;
|
|
float area;
|
|
|
|
d1[0] = b[0] - a[0];
|
|
d1[1] = b[1] - a[1];
|
|
d1[2] = 0;
|
|
|
|
d2[0] = c[0] - a[0];
|
|
d2[1] = c[1] - a[1];
|
|
d2[2] = 0;
|
|
|
|
cross = d1.Cross( d2 );
|
|
area = 0.5 * cross.Length();
|
|
|
|
if ( cross[2] < 0 ) {
|
|
return -area;
|
|
} else {
|
|
return area;
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
RasterizeTriangle
|
|
|
|
It is ok for the texcoords to wrap around, the rasterization
|
|
will deal with it properly.
|
|
================
|
|
*/
|
|
static void RasterizeTriangle( const srfTriangles_t *lowMesh, const idVec3 *lowMeshNormals, int lowFaceNum,
|
|
renderBump_t *rb ) {
|
|
int i, j, k;
|
|
float bounds[2][2];
|
|
float ibounds[2][2];
|
|
float verts[3][2];
|
|
float testVert[2];
|
|
float bary[3];
|
|
byte *localDest, *globalDest, *colorDest;
|
|
float edge[3][3];
|
|
idVec3 sampledNormal;
|
|
byte sampledColor[4] = { };
|
|
idVec3 point, normal, traceNormal, tangents[2];
|
|
float baseArea, totalArea;
|
|
int r, g, b;
|
|
idVec3 localNormal;
|
|
|
|
// this is a brain-dead rasterizer, but compared to the ray trace,
|
|
// nothing we do here is going to matter performance-wise
|
|
|
|
// adjust for resolution and texel centers
|
|
verts[0][0] = lowMesh->verts[ lowMesh->indexes[lowFaceNum*3+0] ].st[0] * rb->width - 0.5;
|
|
verts[1][0] = lowMesh->verts[ lowMesh->indexes[lowFaceNum*3+1] ].st[0] * rb->width - 0.5;
|
|
verts[2][0] = lowMesh->verts[ lowMesh->indexes[lowFaceNum*3+2] ].st[0] * rb->width - 0.5;
|
|
verts[0][1] = lowMesh->verts[ lowMesh->indexes[lowFaceNum*3+0] ].st[1] * rb->height - 0.5;
|
|
verts[1][1] = lowMesh->verts[ lowMesh->indexes[lowFaceNum*3+1] ].st[1] * rb->height - 0.5;
|
|
verts[2][1] = lowMesh->verts[ lowMesh->indexes[lowFaceNum*3+2] ].st[1] * rb->height - 0.5;
|
|
|
|
// find the texcoord bounding box
|
|
bounds[0][0] = 99999;
|
|
bounds[0][1] = 99999;
|
|
bounds[1][0] = -99999;
|
|
bounds[1][1] = -99999;
|
|
for ( i = 0 ; i < 2 ; i++ ) {
|
|
for ( j = 0 ; j < 3 ; j++ ) {
|
|
if ( verts[j][i] < bounds[0][i] ) {
|
|
bounds[0][i] = verts[j][i];
|
|
}
|
|
if ( verts[j][i] > bounds[1][i] ) {
|
|
bounds[1][i] = verts[j][i];
|
|
}
|
|
}
|
|
}
|
|
|
|
// we intentionally rasterize somewhat outside the triangles, so
|
|
// the bilerp support texels (which may be anti-aliased down)
|
|
// are not just duplications of what is on the interior
|
|
const float edgeOverlap = 4.0;
|
|
|
|
ibounds[0][0] = floor( bounds[0][0] - edgeOverlap );
|
|
ibounds[1][0] = ceil( bounds[1][0] + edgeOverlap );
|
|
ibounds[0][1] = floor( bounds[0][1] - edgeOverlap );
|
|
ibounds[1][1] = ceil( bounds[1][1] + edgeOverlap );
|
|
|
|
// calculate edge vectors
|
|
for ( i = 0 ; i < 3 ; i++ ) {
|
|
float *v1, *v2;
|
|
|
|
v1 = verts[i];
|
|
v2 = verts[(i+1)%3];
|
|
|
|
edge[i][0] = v2[1] - v1[1];
|
|
edge[i][1] = v1[0] - v2[0];
|
|
float len = sqrt( edge[i][0] * edge[i][0] + edge[i][1] * edge[i][1] );
|
|
edge[i][0] /= len;
|
|
edge[i][1] /= len;
|
|
edge[i][2] = -( v1[0] * edge[i][0] + v1[1] * edge[i][1] );
|
|
}
|
|
|
|
// itterate over the bounding box, testing against edge vectors
|
|
for ( i = ibounds[0][1] ; i < ibounds[1][1] ; i++ ) {
|
|
for ( j = ibounds[0][0] ; j < ibounds[1][0] ; j++ ) {
|
|
float dists[3];
|
|
|
|
k = ( ( i & (rb->height-1) ) * rb->width + ( j & (rb->width-1) ) ) * 4;
|
|
colorDest = &rb->colorPic[k];
|
|
localDest = &rb->localPic[k];
|
|
globalDest = &rb->globalPic[k];
|
|
|
|
#define SKIP_MIRRORS
|
|
|
|
float *edgeDistance = &rb->edgeDistances[k/4];
|
|
#ifdef SKIP_MIRRORS
|
|
// if this texel has already been filled by a true interior pixel, don't overwrite it
|
|
if ( *edgeDistance == 0 ) {
|
|
continue;
|
|
}
|
|
#endif
|
|
|
|
// check against the three edges to see if the pixel is inside the triangle
|
|
for ( k = 0 ; k < 3 ; k++ ) {
|
|
float v;
|
|
|
|
v = i * edge[k][1] + j * edge[k][0] + edge[k][2];
|
|
dists[k] = v;
|
|
}
|
|
|
|
// the edge polarities might be either way
|
|
if ( ! ( ( dists[0] >= -edgeOverlap && dists[1] >= -edgeOverlap && dists[2] >= -edgeOverlap )
|
|
|| ( dists[0] <= edgeOverlap && dists[1] <= edgeOverlap && dists[2] <= edgeOverlap ) ) ) {
|
|
continue;
|
|
}
|
|
|
|
bool edgeTexel;
|
|
|
|
if ( ( dists[0] >= 0 && dists[1] >= 0 && dists[2] >= 0 )
|
|
|| ( dists[0] <= 0 && dists[1] <= 0 && dists[2] <= 0 ) ) {
|
|
edgeTexel = false;
|
|
} else {
|
|
edgeTexel = true;
|
|
#ifdef SKIP_MIRRORS
|
|
// if this texel has already been filled by another edge pixel, don't overwrite it
|
|
if ( *edgeDistance == 1 ) {
|
|
continue;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// calculate the barycentric coordinates in the triangle for this sample
|
|
testVert[0] = j;
|
|
testVert[1] = i;
|
|
|
|
baseArea = TriTextureArea( verts[0], verts[1], verts[2] );
|
|
bary[0] = TriTextureArea( testVert, verts[1], verts[2] ) / baseArea;
|
|
bary[1] = TriTextureArea( verts[0], testVert, verts[2] ) / baseArea;
|
|
bary[2] = TriTextureArea( verts[0], verts[1], testVert ) / baseArea;
|
|
|
|
totalArea = bary[0] + bary[1] + bary[2];
|
|
if ( totalArea < 0.99 || totalArea > 1.01 ) {
|
|
continue; // should never happen
|
|
}
|
|
|
|
// calculate the interpolated xyz, normal, and tangents of this sample
|
|
point = vec3_origin;
|
|
traceNormal = vec3_origin;
|
|
normal = vec3_origin;
|
|
tangents[0] = vec3_origin;
|
|
tangents[1] = vec3_origin;
|
|
for ( k = 0 ; k < 3 ; k++ ) {
|
|
int index;
|
|
|
|
index = lowMesh->indexes[lowFaceNum*3+k];
|
|
point += bary[k] * lowMesh->verts[ index ].xyz;
|
|
|
|
// traceNormal will differ from normal if the surface uses unsmoothedTangents
|
|
traceNormal += bary[k] * lowMeshNormals[ index ];
|
|
|
|
normal += bary[k] * lowMesh->verts[ index ].normal;
|
|
tangents[0] += bary[k] * lowMesh->verts[ index ].tangents[0];
|
|
tangents[1] += bary[k] * lowMesh->verts[ index ].tangents[1];
|
|
}
|
|
|
|
#if 0
|
|
// this doesn't seem to make much difference
|
|
// an argument can be made that these should not be normalized, because the interpolation
|
|
// of the light position at rasterization time will be linear, not spherical
|
|
normal.Normalize();
|
|
tangents[0].Normalize();
|
|
tangents[1].Normalize();
|
|
#endif
|
|
|
|
// find the best triangle in the high poly model for this
|
|
// sampledNormal will normalized
|
|
if ( !SampleHighMesh( rb, point, traceNormal, sampledNormal, sampledColor ) ) {
|
|
#if 0
|
|
// put bright red where all traces missed for debugging.
|
|
// for production use, it is better to leave it blank so
|
|
// the outlining fills it in
|
|
globalDest[0] = 255;
|
|
globalDest[1] = 0;
|
|
globalDest[2] = 0;
|
|
globalDest[3] = 255;
|
|
|
|
localDest[0] = 255;
|
|
localDest[1] = 0;
|
|
localDest[2] = 0;
|
|
localDest[3] = 255;
|
|
#endif
|
|
continue;
|
|
}
|
|
|
|
|
|
// mark whether this is an interior or edge texel
|
|
*edgeDistance = ( edgeTexel ? 1.0 : 0 );
|
|
|
|
// fill the object space normal map spot
|
|
r = 128 + 127 * sampledNormal[0];
|
|
g = 128 + 127 * sampledNormal[1];
|
|
b = 128 + 127 * sampledNormal[2];
|
|
|
|
globalDest[0] = r;
|
|
globalDest[1] = g;
|
|
globalDest[2] = b;
|
|
globalDest[3] = 255;
|
|
|
|
// transform to local tangent space
|
|
idMat3 mat;
|
|
mat[0] = tangents[0];
|
|
mat[1] = tangents[1];
|
|
mat[2] = normal;
|
|
mat.InverseSelf();
|
|
localNormal = mat * sampledNormal;
|
|
|
|
localNormal.Normalize();
|
|
|
|
|
|
r = 128 + 127 * localNormal[0];
|
|
g = 128 + 127 * localNormal[1];
|
|
b = 128 + 127 * localNormal[2];
|
|
|
|
localDest[0] = r;
|
|
localDest[1] = g;
|
|
localDest[2] = b;
|
|
localDest[3] = 255;
|
|
|
|
colorDest[0] = sampledColor[0];
|
|
colorDest[1] = sampledColor[1];
|
|
colorDest[2] = sampledColor[2];
|
|
colorDest[3] = sampledColor[3];
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
CombineModelSurfaces
|
|
|
|
Frees the model and returns a new model with all triangles combined
|
|
into one surface
|
|
================
|
|
*/
|
|
static idRenderModel *CombineModelSurfaces( idRenderModel *model ) {
|
|
int totalVerts;
|
|
int totalIndexes;
|
|
int numIndexes;
|
|
int numVerts;
|
|
int i, j;
|
|
|
|
totalVerts = 0;
|
|
totalIndexes = 0;
|
|
|
|
for ( i = 0 ; i < model->NumSurfaces() ; i++ ) {
|
|
const modelSurface_t *surf = model->Surface(i);
|
|
|
|
totalVerts += surf->geometry->numVerts;
|
|
totalIndexes += surf->geometry->numIndexes;
|
|
}
|
|
|
|
srfTriangles_t *newTri = R_AllocStaticTriSurf();
|
|
R_AllocStaticTriSurfVerts( newTri, totalVerts );
|
|
R_AllocStaticTriSurfIndexes( newTri, totalIndexes );
|
|
|
|
newTri->numVerts = totalVerts;
|
|
newTri->numIndexes = totalIndexes;
|
|
|
|
newTri->bounds.Clear();
|
|
|
|
idDrawVert *verts = newTri->verts;
|
|
glIndex_t *indexes = newTri->indexes;
|
|
numIndexes = 0;
|
|
numVerts = 0;
|
|
for ( i = 0 ; i < model->NumSurfaces() ; i++ ) {
|
|
const modelSurface_t *surf = model->Surface(i);
|
|
const srfTriangles_t *tri = surf->geometry;
|
|
|
|
memcpy( verts + numVerts, tri->verts, tri->numVerts * sizeof( tri->verts[0] ) );
|
|
for ( j = 0 ; j < tri->numIndexes ; j++ ) {
|
|
indexes[numIndexes+j] = numVerts + tri->indexes[j];
|
|
}
|
|
newTri->bounds.AddBounds( tri->bounds );
|
|
numIndexes += tri->numIndexes;
|
|
numVerts += tri->numVerts;
|
|
}
|
|
|
|
modelSurface_t surf;
|
|
|
|
surf.id = 0;
|
|
surf.geometry = newTri;
|
|
surf.shader = tr.defaultMaterial;
|
|
|
|
idRenderModel *newModel = renderModelManager->AllocModel();
|
|
newModel->AddSurface( surf );
|
|
|
|
renderModelManager->FreeModel( model );
|
|
|
|
return newModel;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
RenderBumpTriangles
|
|
|
|
==============
|
|
*/
|
|
static void RenderBumpTriangles( srfTriangles_t *lowMesh, renderBump_t *rb ) {
|
|
int i, j;
|
|
|
|
RB_SetGL2D();
|
|
|
|
qglDisable( GL_CULL_FACE );
|
|
|
|
qglColor3f( 1, 1, 1 );
|
|
|
|
qglMatrixMode( GL_PROJECTION );
|
|
qglLoadIdentity();
|
|
qglOrtho( 0, 1, 1, 0, -1, 1 );
|
|
qglDisable( GL_BLEND );
|
|
qglMatrixMode( GL_MODELVIEW );
|
|
qglLoadIdentity();
|
|
|
|
qglDisable( GL_DEPTH_TEST );
|
|
|
|
qglClearColor(1,0,0,1);
|
|
qglClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
|
|
|
|
qglColor3f( 1, 1, 1 );
|
|
|
|
// create smoothed normals for the surface, which might be
|
|
// different than the normals at the vertexes if the
|
|
// surface uses unsmoothedNormals, which only takes the
|
|
// normal from a single triangle. We need properly smoothed
|
|
// normals to make sure that the traces always go off normal
|
|
// to the true surface.
|
|
idVec3 *lowMeshNormals = (idVec3 *)Mem_ClearedAlloc( lowMesh->numVerts * sizeof( *lowMeshNormals ) );
|
|
R_DeriveFacePlanes( lowMesh );
|
|
R_CreateSilIndexes( lowMesh ); // recreate, merging the mirrored verts back together
|
|
const idPlane *planes = lowMesh->facePlanes;
|
|
for ( i = 0 ; i < lowMesh->numIndexes ; i += 3, planes++ ) {
|
|
for ( j = 0 ; j < 3 ; j++ ) {
|
|
int index;
|
|
|
|
index = lowMesh->silIndexes[i+j];
|
|
lowMeshNormals[index] += (*planes).Normal();
|
|
}
|
|
}
|
|
// normalize and replicate from silIndexes to all indexes
|
|
for ( i = 0 ; i < lowMesh->numIndexes ; i++ ) {
|
|
lowMeshNormals[lowMesh->indexes[i]] = lowMeshNormals[lowMesh->silIndexes[i]];
|
|
lowMeshNormals[lowMesh->indexes[i]].Normalize();
|
|
}
|
|
|
|
|
|
// rasterize each low poly face
|
|
for ( j = 0 ; j < lowMesh->numIndexes ; j+=3 ) {
|
|
// pump the event loop so the window can be dragged around
|
|
Sys_GenerateEvents();
|
|
|
|
RasterizeTriangle( lowMesh, lowMeshNormals, j/3, rb );
|
|
|
|
qglClearColor(1,0,0,1);
|
|
qglClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
|
|
qglRasterPos2f( 0, 1 );
|
|
qglPixelZoom( glConfig.vidWidth / (float)rb->width, glConfig.vidHeight / (float)rb->height );
|
|
qglDrawPixels( rb->width, rb->height, GL_RGBA, GL_UNSIGNED_BYTE, rb->localPic );
|
|
qglPixelZoom( 1, 1 );
|
|
qglFlush();
|
|
GLimp_SwapBuffers();
|
|
}
|
|
|
|
Mem_Free( lowMeshNormals );
|
|
}
|
|
|
|
|
|
/*
|
|
==============
|
|
WriteRenderBump
|
|
|
|
==============
|
|
*/
|
|
static void WriteRenderBump( renderBump_t *rb, int outLinePixels ) {
|
|
int width, height;
|
|
int i;
|
|
idStr filename;
|
|
|
|
renderModelManager->FreeModel( rb->highModel );
|
|
|
|
FreeTriHash( rb->hash );
|
|
|
|
width = rb->width;
|
|
height = rb->height;
|
|
|
|
#if 0
|
|
// save the non-outlined version
|
|
filename = source;
|
|
filename.setFileExtension();
|
|
filename.append( "_nooutline.tga" );
|
|
common->Printf( "writing %s\n", filename.c_str() );
|
|
WriteTGA( filename, globalPic, width, height );
|
|
#endif
|
|
|
|
// outline the image several times to help bilinear filtering across disconnected
|
|
// edges, and mip-mapping
|
|
for ( i = 0 ; i < outLinePixels ; i++ ) {
|
|
OutlineNormalMap( rb->localPic, width, height, 128, 128, 128 );
|
|
OutlineNormalMap( rb->globalPic, width, height, 128, 128, 128 );
|
|
OutlineColorMap( rb->colorPic, width, height, 128, 128, 128 );
|
|
}
|
|
|
|
// filter down if we are anti-aliasing
|
|
for ( i = 0 ; i < rb->antiAlias ; i++ ) {
|
|
byte *old;
|
|
|
|
old = rb->localPic;
|
|
rb->localPic = R_MipMap( rb->localPic, width, height, false );
|
|
Mem_Free( old );
|
|
|
|
old = rb->globalPic;
|
|
rb->globalPic = R_MipMap( rb->globalPic, width, height, false );
|
|
Mem_Free( old );
|
|
|
|
old = rb->colorPic;
|
|
rb->colorPic = R_MipMap( rb->colorPic, width, height, false );
|
|
Mem_Free( old );
|
|
|
|
width >>= 1;
|
|
height >>= 1;
|
|
}
|
|
|
|
// write out the local map
|
|
filename = rb->outputName;
|
|
filename.SetFileExtension( ".tga" );
|
|
common->Printf( "writing %s (%i,%i)\n", filename.c_str(), width, height );
|
|
R_WriteTGA( filename, rb->localPic, width, height );
|
|
|
|
if ( rb->saveGlobalMap ) {
|
|
filename = rb->outputName;
|
|
filename.StripFileExtension();
|
|
filename.Append( "_global.tga" );
|
|
common->Printf( "writing %s (%i,%i)\n", filename.c_str(), width, height );
|
|
R_WriteTGA( filename, rb->globalPic, width, height );
|
|
}
|
|
|
|
if ( rb->saveColorMap ) {
|
|
filename = rb->outputName;
|
|
filename.StripFileExtension();
|
|
filename.Append( "_color.tga" );
|
|
common->Printf( "writing %s (%i,%i)\n", filename.c_str(), width, height );
|
|
R_WriteTGA( filename, rb->colorPic, width, height );
|
|
}
|
|
|
|
Mem_Free( rb->localPic );
|
|
Mem_Free( rb->globalPic );
|
|
Mem_Free( rb->colorPic );
|
|
Mem_Free( rb->edgeDistances );
|
|
}
|
|
|
|
/*
|
|
===============
|
|
InitRenderBump
|
|
===============
|
|
*/
|
|
static void InitRenderBump( renderBump_t *rb ) {
|
|
srfTriangles_t *mesh;
|
|
idBounds bounds;
|
|
int i, c;
|
|
|
|
// load the ase file
|
|
common->Printf( "loading %s...\n", rb->highName );
|
|
|
|
rb->highModel = renderModelManager->AllocModel();
|
|
rb->highModel->PartialInitFromFile( rb->highName );
|
|
if ( !rb->highModel ) {
|
|
common->Error( "failed to load %s", rb->highName );
|
|
}
|
|
|
|
// combine the high poly model into a single polyset
|
|
if ( rb->highModel->NumSurfaces() != 1 ) {
|
|
rb->highModel = CombineModelSurfaces( rb->highModel );
|
|
}
|
|
|
|
const modelSurface_t *surf = rb->highModel->Surface( 0 );
|
|
mesh = surf->geometry;
|
|
|
|
rb->mesh = mesh;
|
|
|
|
R_DeriveFacePlanes( mesh );
|
|
|
|
// create a face hash table to accelerate the tracing
|
|
rb->hash = CreateTriHash( mesh );
|
|
|
|
// bound the entire file
|
|
R_BoundTriSurf( mesh );
|
|
bounds = mesh->bounds;
|
|
|
|
// the traceDist will be the traceFrac times the larges bounds axis
|
|
rb->traceDist = 0;
|
|
for ( i = 0 ; i < 3 ; i++ ) {
|
|
float d;
|
|
|
|
d = rb->traceFrac * ( bounds[1][i] - bounds[0][i] );
|
|
if ( d > rb->traceDist ) {
|
|
rb->traceDist = d;
|
|
}
|
|
}
|
|
common->Printf( "trace fraction %4.2f = %6.2f model units\n", rb->traceFrac, rb->traceDist );
|
|
|
|
c = rb->width * rb->height * 4;
|
|
|
|
// local normal map
|
|
rb->localPic = (byte *)Mem_Alloc( c );
|
|
|
|
// global (object space, not surface space) normal map
|
|
rb->globalPic = (byte *)Mem_Alloc( c );
|
|
|
|
// color pic for artist reference
|
|
rb->colorPic = (byte *)Mem_Alloc( c );
|
|
|
|
// edgeDistance for marking outside-the-triangle traces
|
|
rb->edgeDistances = (float *)Mem_Alloc( c );
|
|
|
|
for ( i = 0 ; i < c ; i+=4 ) {
|
|
rb->localPic[i+0] = 128;
|
|
rb->localPic[i+1] = 128;
|
|
rb->localPic[i+2] = 128;
|
|
rb->localPic[i+3] = 0; // the artists use this for masking traced pixels sometimes
|
|
|
|
rb->globalPic[i+0] = 128;
|
|
rb->globalPic[i+1] = 128;
|
|
rb->globalPic[i+2] = 128;
|
|
rb->globalPic[i+3] = 0;
|
|
|
|
rb->colorPic[i+0] = 128;
|
|
rb->colorPic[i+1] = 128;
|
|
rb->colorPic[i+2] = 128;
|
|
rb->colorPic[i+3] = 0;
|
|
|
|
rb->edgeDistances[i/4] = -1; // not traced yet
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
|
==============
|
|
RenderBump_f
|
|
|
|
==============
|
|
*/
|
|
void RenderBump_f( const idCmdArgs &args ) {
|
|
idRenderModel *lowPoly;
|
|
idStr source;
|
|
int i, j;
|
|
const char *cmdLine;
|
|
int numRenderBumps;
|
|
renderBump_t *renderBumps, *rb = NULL;
|
|
renderBump_t opt;
|
|
int startTime, endTime;
|
|
|
|
// update the screen as we print
|
|
common->SetRefreshOnPrint( true );
|
|
|
|
// there should be a single parameter, the filename for a game loadable low-poly model
|
|
if ( args.Argc() != 2 ) {
|
|
common->Error( "Usage: renderbump <lowPolyModel>" );
|
|
}
|
|
|
|
common->Printf( "----- Renderbump %s -----\n", args.Argv( 1 ) );
|
|
|
|
startTime = Sys_Milliseconds();
|
|
|
|
// get the lowPoly model
|
|
source = args.Argv( 1 );
|
|
lowPoly = renderModelManager->CheckModel( source );
|
|
if ( !lowPoly ) {
|
|
common->Error( "Can't load model %s", source.c_str() );
|
|
}
|
|
|
|
renderBumps = (renderBump_t *)R_StaticAlloc( lowPoly->NumSurfaces() * sizeof( *renderBumps ) );
|
|
numRenderBumps = 0;
|
|
for ( i = 0 ; i < lowPoly->NumSurfaces() ; i++ ) {
|
|
const modelSurface_t *ms = lowPoly->Surface( i );
|
|
|
|
// default options
|
|
memset( &opt, 0, sizeof( opt ) );
|
|
opt.width = 512;
|
|
opt.height = 512;
|
|
opt.antiAlias = 1;
|
|
opt.outline = 8;
|
|
opt.traceFrac = 0.05f;
|
|
|
|
// parse the renderbump parameters for this surface
|
|
cmdLine = ms->shader->GetRenderBump();
|
|
|
|
common->Printf( "surface %i, shader %s\nrenderBump = %s ", i,
|
|
ms->shader->GetName(), cmdLine );
|
|
|
|
if ( !ms->geometry ) {
|
|
common->Printf( "(no geometry)\n" );
|
|
continue;
|
|
}
|
|
|
|
idCmdArgs localArgs;
|
|
localArgs.TokenizeString( cmdLine, false );
|
|
|
|
if ( localArgs.Argc() < 2 ) {
|
|
common->Printf( "(no action)\n" );
|
|
continue;
|
|
}
|
|
|
|
common->Printf( "(rendering)\n" );
|
|
|
|
for ( j = 0 ; j < localArgs.Argc() - 2; j++ ) {
|
|
const char *s;
|
|
|
|
s = localArgs.Argv( j );
|
|
if ( s[0] == '-' ) {
|
|
j++;
|
|
s = localArgs.Argv( j );
|
|
if ( s[0] == '\0' ) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ( !idStr::Icmp( s, "size" ) ) {
|
|
if ( j + 2 >= localArgs.Argc() ) {
|
|
j = localArgs.Argc();
|
|
break;
|
|
}
|
|
opt.width = atoi( localArgs.Argv( j + 1 ) );
|
|
opt.height = atoi( localArgs.Argv( j + 2 ) );
|
|
j += 2;
|
|
} else if ( !idStr::Icmp( s, "trace" ) ) {
|
|
opt.traceFrac = atof( localArgs.Argv( j + 1 ) );
|
|
j += 1;
|
|
} else if ( !idStr::Icmp( s, "globalMap" ) ) {
|
|
opt.saveGlobalMap = true;
|
|
} else if ( !idStr::Icmp( s, "colorMap" ) ) {
|
|
opt.saveColorMap = true;
|
|
} else if ( !idStr::Icmp( s, "outline" ) ) {
|
|
opt.outline = atoi( localArgs.Argv( j + 1 ) );
|
|
j += 1;
|
|
} else if ( !idStr::Icmp( s, "aa" ) ) {
|
|
opt.antiAlias = atoi( localArgs.Argv( j + 1 ) );
|
|
j += 1;
|
|
} else {
|
|
common->Printf( "WARNING: Unknown option \"%s\"\n", s );
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( j != ( localArgs.Argc() - 2 ) ) {
|
|
common->Error( "usage: renderBump [-size width height] [-aa <1-2>] [globalMap] [colorMap] [-trace <0.01 - 1.0>] normalMapImageFile highPolyAseFile" );
|
|
}
|
|
idStr::Copynz( opt.outputName, localArgs.Argv( j ), sizeof( opt.outputName ) );
|
|
idStr::Copynz( opt.highName, localArgs.Argv( j + 1 ), sizeof( opt.highName ) );
|
|
|
|
// adjust size for anti-aliasing
|
|
opt.width <<= opt.antiAlias;
|
|
opt.height <<= opt.antiAlias;
|
|
|
|
// see if we already have a renderbump going for another surface that this should use
|
|
for ( j = 0 ; j < numRenderBumps ; j++ ) {
|
|
rb = &renderBumps[j];
|
|
|
|
if ( idStr::Icmp( rb->outputName, opt.outputName ) ) {
|
|
continue;
|
|
}
|
|
// all the other parameters must match, or it is an error
|
|
if ( idStr::Icmp( rb->highName, opt.highName) || rb->width != opt.width ||
|
|
rb->height != opt.height || rb->antiAlias != opt.antiAlias ||
|
|
rb->traceFrac != opt.traceFrac ) {
|
|
common->Error( "mismatched renderbump parameters on image %s", rb->outputName );
|
|
continue;
|
|
}
|
|
|
|
// saveGlobalMap will be a sticky option
|
|
rb->saveGlobalMap = rb->saveGlobalMap | opt.saveGlobalMap;
|
|
break;
|
|
}
|
|
|
|
// create a new renderbump if needed
|
|
if ( j == numRenderBumps ) {
|
|
numRenderBumps++;
|
|
rb = &renderBumps[j];
|
|
*rb = opt;
|
|
|
|
InitRenderBump( rb );
|
|
}
|
|
|
|
// render the triangles for this surface
|
|
RenderBumpTriangles( ms->geometry, rb );
|
|
}
|
|
|
|
//
|
|
// anti-alias and write out all renderbumps that we have completed
|
|
//
|
|
for ( i = 0 ; i < numRenderBumps ; i++ ) {
|
|
WriteRenderBump( &renderBumps[i], opt.outline << opt.antiAlias );
|
|
}
|
|
|
|
R_StaticFree( renderBumps );
|
|
|
|
endTime = Sys_Milliseconds();
|
|
common->Printf( "%5.2f seconds for renderBump\n", ( endTime - startTime ) / 1000.0 );
|
|
common->Printf( "---------- RenderBump Completed ----------\n" );
|
|
|
|
// stop updating the screen as we print
|
|
common->SetRefreshOnPrint( false );
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
==================================================================================
|
|
|
|
FLAT
|
|
|
|
The flat case is trivial, and accomplished with hardware rendering
|
|
|
|
==================================================================================
|
|
*/
|
|
|
|
|
|
/*
|
|
==============
|
|
RenderBumpFlat_f
|
|
|
|
==============
|
|
*/
|
|
void RenderBumpFlat_f( const idCmdArgs &args ) {
|
|
int width, height;
|
|
idStr source;
|
|
int i;
|
|
idBounds bounds;
|
|
srfTriangles_t *mesh;
|
|
|
|
// update the screen as we print
|
|
common->SetRefreshOnPrint( true );
|
|
|
|
width = height = 256;
|
|
|
|
// check options
|
|
for ( i = 1 ; i < args.Argc() - 1; i++ ) {
|
|
const char *s;
|
|
|
|
s = args.Argv( i );
|
|
if ( s[0] == '-' ) {
|
|
i++;
|
|
s = args.Argv( i );
|
|
}
|
|
|
|
if ( !idStr::Icmp( s, "size" ) ) {
|
|
if ( i + 2 >= args.Argc() ) {
|
|
i = args.Argc();
|
|
break;
|
|
}
|
|
width = atoi( args.Argv( i + 1 ) );
|
|
height = atoi( args.Argv( i + 2 ) );
|
|
i += 2;
|
|
} else {
|
|
common->Printf( "WARNING: Unknown option \"%s\"\n", s );
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( i != ( args.Argc() - 1 ) ) {
|
|
common->Error( "usage: renderBumpFlat [-size width height] asefile" );
|
|
return;
|
|
}
|
|
|
|
common->Printf( "Final image size: %i, %i\n", width, height );
|
|
|
|
// load the source in "fastload" mode, because we don't
|
|
// need tangent and shadow information
|
|
source = args.Argv( i );
|
|
|
|
idRenderModel *highPolyModel = renderModelManager->AllocModel();
|
|
|
|
highPolyModel->PartialInitFromFile( source );
|
|
|
|
if ( highPolyModel->IsDefaultModel() ) {
|
|
common->Error( "failed to load %s", source.c_str() );
|
|
}
|
|
|
|
// combine the high poly model into a single polyset
|
|
if ( highPolyModel->NumSurfaces() != 1 ) {
|
|
highPolyModel = CombineModelSurfaces( highPolyModel );
|
|
}
|
|
|
|
// create normals if not present in file
|
|
const modelSurface_t *surf = highPolyModel->Surface( 0 );
|
|
mesh = surf->geometry;
|
|
|
|
// bound the entire file
|
|
R_BoundTriSurf( mesh );
|
|
bounds = mesh->bounds;
|
|
|
|
SaveWindow();
|
|
ResizeWindow( width, height );
|
|
|
|
// for small images, the viewport may be less than the minimum window
|
|
qglViewport( 0, 0, width, height );
|
|
|
|
qglEnable( GL_CULL_FACE );
|
|
qglCullFace( GL_FRONT );
|
|
qglDisable( GL_STENCIL_TEST );
|
|
qglDisable( GL_SCISSOR_TEST );
|
|
qglDisable( GL_ALPHA_TEST );
|
|
qglDisable( GL_BLEND );
|
|
qglEnable( GL_DEPTH_TEST );
|
|
qglDisable( GL_TEXTURE_2D );
|
|
qglDepthMask( GL_TRUE );
|
|
qglDepthFunc( GL_LEQUAL );
|
|
|
|
qglColor3f( 1, 1, 1 );
|
|
|
|
qglMatrixMode( GL_PROJECTION );
|
|
qglLoadIdentity();
|
|
qglOrtho( bounds[0][0], bounds[1][0], bounds[0][2],
|
|
bounds[1][2], -( bounds[0][1] - 1 ), -( bounds[1][1] + 1 ) );
|
|
|
|
qglMatrixMode( GL_MODELVIEW );
|
|
qglLoadIdentity();
|
|
|
|
// flat maps are automatically anti-aliased
|
|
|
|
idStr filename;
|
|
int j, k, c;
|
|
byte *buffer;
|
|
int *sumBuffer, *colorSumBuffer;
|
|
bool flat;
|
|
int sample;
|
|
|
|
sumBuffer = (int *)Mem_Alloc( width * height * 4 * 4 );
|
|
memset( sumBuffer, 0, width * height * 4 * 4 );
|
|
buffer = (byte *)Mem_Alloc( width * height * 4 );
|
|
|
|
colorSumBuffer = (int *)Mem_Alloc( width * height * 4 * 4 );
|
|
memset( sumBuffer, 0, width * height * 4 * 4 );
|
|
|
|
flat = false;
|
|
//flat = true;
|
|
|
|
for ( sample = 0 ; sample < 16 ; sample++ ) {
|
|
float xOff, yOff;
|
|
|
|
xOff = ( ( sample & 3 ) / 4.0 ) * ( bounds[1][0] - bounds[0][0] ) / width;
|
|
yOff = ( ( sample / 4 ) / 4.0 ) * ( bounds[1][2] - bounds[0][2] ) / height;
|
|
|
|
for ( int colorPass = 0 ; colorPass < 2 ; colorPass++ ) {
|
|
qglClearColor(0.5,0.5,0.5,0);
|
|
qglClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
|
|
|
|
qglBegin( GL_TRIANGLES );
|
|
for ( i = 0 ; i < highPolyModel->NumSurfaces() ; i++ ) {
|
|
const modelSurface_t *surf = highPolyModel->Surface( i );
|
|
|
|
mesh = surf->geometry;
|
|
|
|
if ( colorPass ) {
|
|
// just render the surface color for artist visualization
|
|
for ( j = 0 ; j < mesh->numIndexes ; j+=3 ) {
|
|
for ( k = 0 ; k < 3 ; k++ ) {
|
|
int v;
|
|
float *a;
|
|
|
|
v = mesh->indexes[j+k];
|
|
qglColor3ubv( mesh->verts[v].color );
|
|
a = mesh->verts[v].xyz.ToFloatPtr();
|
|
qglVertex3f( a[0] + xOff, a[2] + yOff, a[1] );
|
|
}
|
|
}
|
|
} else {
|
|
// render as normal map
|
|
// we can either flat shade from the plane,
|
|
// or smooth shade from the vertex normals
|
|
for ( j = 0 ; j < mesh->numIndexes ; j+=3 ) {
|
|
if ( flat ) {
|
|
idPlane plane;
|
|
idVec3 *a, *b, *c;
|
|
int v1, v2, v3;
|
|
|
|
v1 = mesh->indexes[j+0];
|
|
v2 = mesh->indexes[j+1];
|
|
v3 = mesh->indexes[j+2];
|
|
|
|
a = &mesh->verts[ v1 ].xyz;
|
|
b = &mesh->verts[ v2 ].xyz;
|
|
c = &mesh->verts[ v3 ].xyz;
|
|
|
|
plane.FromPoints( *a, *b, *c );
|
|
|
|
// NULLNORMAL is used by the artists to force an area to reflect no
|
|
// light at all
|
|
if ( surf->shader->GetSurfaceFlags() & SURF_NULLNORMAL ) {
|
|
qglColor3f( 0.5, 0.5, 0.5 );
|
|
} else {
|
|
qglColor3f( 0.5 + 0.5*plane[0], 0.5 - 0.5*plane[2], 0.5 - 0.5*plane[1] );
|
|
}
|
|
|
|
qglVertex3f( (*a)[0] + xOff, (*a)[2] + yOff, (*a)[1] );
|
|
qglVertex3f( (*b)[0] + xOff, (*b)[2] + yOff, (*b)[1] );
|
|
qglVertex3f( (*c)[0] + xOff, (*c)[2] + yOff, (*c)[1] );
|
|
} else {
|
|
for ( k = 0 ; k < 3 ; k++ ) {
|
|
int v;
|
|
float *n;
|
|
float *a;
|
|
|
|
v = mesh->indexes[j+k];
|
|
n = mesh->verts[v].normal.ToFloatPtr();
|
|
|
|
// NULLNORMAL is used by the artists to force an area to reflect no
|
|
// light at all
|
|
if ( surf->shader->GetSurfaceFlags() & SURF_NULLNORMAL ) {
|
|
qglColor3f( 0.5, 0.5, 0.5 );
|
|
} else {
|
|
// we are going to flip the normal Z direction
|
|
qglColor3f( 0.5 + 0.5*n[0], 0.5 - 0.5*n[2], 0.5 - 0.5*n[1] );
|
|
}
|
|
|
|
a = mesh->verts[v].xyz.ToFloatPtr();
|
|
qglVertex3f( a[0] + xOff, a[2] + yOff, a[1] );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
qglEnd();
|
|
qglFlush();
|
|
GLimp_SwapBuffers();
|
|
qglReadPixels( 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer );
|
|
|
|
c = width * height;
|
|
|
|
if ( colorPass ) {
|
|
// add to the sum buffer
|
|
for ( i = 0 ; i < c ; i++ ) {
|
|
colorSumBuffer[i*4+0] += buffer[i*4+0];
|
|
colorSumBuffer[i*4+1] += buffer[i*4+1];
|
|
colorSumBuffer[i*4+2] += buffer[i*4+2];
|
|
colorSumBuffer[i*4+3] += buffer[i*4+3];
|
|
}
|
|
} else {
|
|
// normalize
|
|
for ( i = 0 ; i < c ; i++ ) {
|
|
idVec3 v;
|
|
|
|
v[0] = ( buffer[i*4+0] - 128 ) / 127.0;
|
|
v[1] = ( buffer[i*4+1] - 128 ) / 127.0;
|
|
v[2] = ( buffer[i*4+2] - 128 ) / 127.0;
|
|
|
|
v.Normalize();
|
|
|
|
buffer[i*4+0] = 128 + 127 * v[0];
|
|
buffer[i*4+1] = 128 + 127 * v[1];
|
|
buffer[i*4+2] = 128 + 127 * v[2];
|
|
}
|
|
|
|
// outline into non-drawn areas
|
|
for ( i = 0 ; i < 8 ; i++ ) {
|
|
OutlineNormalMap( buffer, width, height, 128, 128, 128 );
|
|
}
|
|
|
|
// add to the sum buffer
|
|
for ( i = 0 ; i < c ; i++ ) {
|
|
sumBuffer[i*4+0] += buffer[i*4+0];
|
|
sumBuffer[i*4+1] += buffer[i*4+1];
|
|
sumBuffer[i*4+2] += buffer[i*4+2];
|
|
sumBuffer[i*4+3] += buffer[i*4+3];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
c = width * height;
|
|
|
|
// save out the color map
|
|
for ( i = 0 ; i < c ; i++ ) {
|
|
buffer[i*4+0] = colorSumBuffer[i*4+0] / 16;
|
|
buffer[i*4+1] = colorSumBuffer[i*4+1] / 16;
|
|
buffer[i*4+2] = colorSumBuffer[i*4+2] / 16;
|
|
buffer[i*4+3] = colorSumBuffer[i*4+3] / 16;
|
|
}
|
|
filename = source;
|
|
filename.StripFileExtension();
|
|
filename.Append( "_color.tga" );
|
|
R_VerticalFlip( buffer, width, height );
|
|
R_WriteTGA( filename, buffer, width, height );
|
|
|
|
// save out the local map
|
|
// scale the sum buffer back down to the sample buffer
|
|
// we allow this to denormalize
|
|
for ( i = 0 ; i < c ; i++ ) {
|
|
buffer[i*4+0] = sumBuffer[i*4+0] / 16;
|
|
buffer[i*4+1] = sumBuffer[i*4+1] / 16;
|
|
buffer[i*4+2] = sumBuffer[i*4+2] / 16;
|
|
buffer[i*4+3] = sumBuffer[i*4+3] / 16;
|
|
}
|
|
|
|
filename = source;
|
|
filename.StripFileExtension();
|
|
filename.Append( "_local.tga" );
|
|
common->Printf( "writing %s (%i,%i)\n", filename.c_str(), width, height );
|
|
R_VerticalFlip( buffer, width, height );
|
|
R_WriteTGA( filename, buffer, width, height );
|
|
|
|
|
|
// free the model
|
|
renderModelManager->FreeModel( highPolyModel );
|
|
|
|
// free our work buffer
|
|
Mem_Free( buffer );
|
|
Mem_Free( sumBuffer );
|
|
Mem_Free( colorSumBuffer );
|
|
|
|
RestoreWindow();
|
|
|
|
// stop updating the screen as we print
|
|
common->SetRefreshOnPrint( false );
|
|
|
|
common->Error( "Completed." );
|
|
}
|