dhewm3/neo/tools/compilers/renderbump/renderbump.cpp
dhewg 736ec20d4d Untangle the epic precompiled.h mess
Don't include the lazy precompiled.h everywhere, only what's
required for the compilation unit.
platform.h needs to be included instead to provide all essential
defines and types.
All includes use the relative path to the neo or the game
specific root.
Move all idlib related includes from idlib/Lib.h to precompiled.h.
precompiled.h still exists for the MFC stuff in tools/.
Add some missing header guards.
2011-12-19 23:21:47 +01:00

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 ) {
#ifdef WIN32
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 );
qwglMakeCurrent( win32.hDC, win32.hGLRC );
#endif
}
/*
===============
RestoreWindow
===============
*/
static void RestoreWindow( void ) {
#ifdef WIN32
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." );
}