dhewm3/neo/tools/compilers/renderbump/renderbump.cpp
2011-12-10 15:34:48 +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 "../../../idlib/precompiled.h"
#pragma hdrstop
#ifdef WIN32
#include <windows.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include "../../../sys/win32/win_local.h"
#endif
#include "../../../renderer/tr_local.h"
/*
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 float traceFraction;
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;
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;
float boundsScale;
// update the screen as we print
common->SetRefreshOnPrint( true );
width = height = 256;
boundsScale = 0;
// 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" );
}
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 );
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
c = width * height;
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." );
}