xash3d-fwgs/ref/vk/vk_brush.c
Ivan Avdeev a2661fe95d vk: add skybox pipeline for traditional renderer
It is not wired up to render anything yet, but we just made sure that it
builds and gets destroyed.

Also, fixup dynamic BLAS double-free.
2024-02-05 10:57:50 -05:00

1987 lines
64 KiB
C

#include "vk_brush.h"
#include "vk_core.h"
#include "vk_const.h"
#include "vk_math.h"
#include "r_textures.h"
#include "vk_lightmap.h"
#include "vk_render.h"
#include "vk_geometry.h"
#include "vk_light.h"
#include "vk_mapents.h"
#include "r_speeds.h"
#include "vk_staging.h"
#include "vk_logs.h"
#include "profiler.h"
#include <math.h>
#include <memory.h>
#define MODULE_NAME "brush"
#define LOG_MODULE brush
typedef struct {
int surfaces_count;
const int *surfaces_indices;
r_geometry_range_t geometry;
vk_render_model_t render_model;
} r_brush_water_model_t;
typedef struct {
float texture_width;
int vertices_count;
int vertices_src_offset;
int vertices_dst_offset;
int geometry_index;
} r_conveyor_t;
typedef struct vk_brush_model_s {
model_t *engine_model;
int patch_rendermode;
r_geometry_range_t geometry;
vk_render_model_t render_model;
int *surface_to_geometry_index;
int *animated_indexes;
int animated_indexes_count;
matrix4x4 prev_transform;
float prev_time;
r_brush_water_model_t water;
r_brush_water_model_t water_sides;
int conveyors_count;
r_conveyor_t *conveyors;
vk_vertex_t *conveyors_vertices;
// Polylights which need to be added per-frame dynamically
struct rt_light_add_polygon_s *dynamic_polylights;
int dynamic_polylights_count;
} vk_brush_model_t;
typedef struct {
int surfaces;
int vertices;
int indices;
} water_model_sizes_t;
typedef struct {
int num_surfaces, num_vertices, num_indices;
int max_texture_id;
int animated_count;
int conveyors_count;
int conveyors_vertices_count;
int sky_surfaces_count;
water_model_sizes_t water, side_water;
} model_sizes_t;
typedef struct conn_edge_s {
int first_surface;
int count;
} conn_edge_t;
typedef struct linked_value_s {
int value, link;
} linked_value_t;
#define MAX_VERTEX_SURFACES 16
typedef struct conn_vertex_s {
int count;
linked_value_t surfs[MAX_VERTEX_SURFACES];
} conn_vertex_t;
static struct {
struct {
int total_vertices, total_indices;
int models_drawn;
int water_surfaces_drawn;
int water_polys_drawn;
} stat;
int rtable[MOD_FRAMES][MOD_FRAMES];
// Unfortunately the engine only tracks the toplevel worldmodel. *xx submodels, while having their own entities and models, are not lifetime-tracked.
// I.e. the engine doesn't call Mod_ProcessRenderData() on them, so we don't directly know when to create or destroy them.
// Therefore, we need to track them manually and destroy them based on some other external event, e.g. Mod_ProcessRenderData(worldmodel)
vk_brush_model_t *models[MAX_MODELS];
int models_count;
#define MAX_ANIMATED_TEXTURES 256
int updated_textures[MAX_ANIMATED_TEXTURES];
// Smoothed normals comptutation
// Connectome for edges and vertices
struct {
int edges_capacity;
conn_edge_t *edges;
int vertices_capacity;
conn_vertex_t *vertices;
} conn;
} g_brush;
void VK_InitRandomTable( void )
{
int tu, tv;
// make random predictable
gEngine.COM_SetRandomSeed( 255 );
for( tu = 0; tu < MOD_FRAMES; tu++ )
{
for( tv = 0; tv < MOD_FRAMES; tv++ )
{
g_brush.rtable[tu][tv] = gEngine.COM_RandomLong( 0, 0x7FFF );
}
}
gEngine.COM_SetRandomSeed( 0 );
}
qboolean R_BrushInit( void )
{
VK_InitRandomTable ();
R_SPEEDS_COUNTER(g_brush.stat.models_drawn, "drawn", kSpeedsMetricCount);
R_SPEEDS_COUNTER(g_brush.stat.water_surfaces_drawn, "water.surfaces", kSpeedsMetricCount);
R_SPEEDS_COUNTER(g_brush.stat.water_polys_drawn, "water.polys", kSpeedsMetricCount);
return true;
}
void R_BrushShutdown( void ) {
if (g_brush.conn.edges)
Mem_Free(g_brush.conn.edges);
}
// speed up sin calculations
static const float r_turbsin[] =
{
#include "warpsin.h"
};
#define SUBDIVIDE_SIZE 64
#define TURBSCALE ( 256.0f / ( M_PI2 ))
static void addWarpVertIndCounts(const msurface_t *warp, int *num_vertices, int *num_indices) {
for( const glpoly_t *p = warp->polys; p; p = p->next ) {
const int triangles = p->numverts - 2;
*num_vertices += p->numverts;
*num_indices += triangles * 3;
}
}
typedef struct {
float prev_time;
float wave_height;
const msurface_t *warp;
vk_vertex_t *dst_vertices;
uint16_t *dst_indices;
vk_render_geometry_t *dst_geometry;
int *out_vertex_count, *out_index_count;
qboolean debug;
} compute_water_polys_t;
#if 0
static qboolean tesselationHasSameOrientation( const msurface_t *surf, qboolean debug ) {
const glpoly_t *poly = surf->polys;
ASSERT(poly);
ASSERT(poly->numverts > 2);
const float *v = poly->verts[0];
const float *const v0 = poly->verts[0];
const float *const v1 = poly->verts[1];
const float *const v2 = poly->verts[2];
vec3_t e0, e1, normal;
VectorSubtract( v2, v0, e0 );
VectorSubtract( v1, v0, e1 );
/* if (surf->flags & SURF_PLANEBACK) */
/* CrossProduct( e1, e0, normal ); */
/* else */
CrossProduct( e0, e1, normal );
// Debug only
VectorNormalize(normal);
const float dot = DotProduct(normal, surf->plane->normal);
const qboolean same = dot > 0;
if (debug)
DEBUG(" surf=%p back=%d plane=(%f, %f, %f), poly=(%f, %f, %f) dot=%f same=%d",
surf, !!(surf->flags & SURF_PLANEBACK),
surf->plane->normal[0], surf->plane->normal[1], surf->plane->normal[2],
normal[0], normal[1], normal[2],
dot, same
);
return same;
}
#endif
static void brushComputeWaterPolys( compute_water_polys_t args ) {
const float time = gpGlobals->time;
const qboolean reverse = false;//!tesselationHasSameOrientation( args.warp, args.debug );
#define MAX_WATER_VERTICES 16
vk_vertex_t poly_vertices[MAX_WATER_VERTICES];
// FIXME unused? const qboolean useQuads = FBitSet( warp->flags, SURF_DRAWTURB_QUADS );
ASSERT(args.warp->polys);
// reset fog color for nonlightmapped water
// FIXME VK GL_ResetFogColor();
int vertices = 0;
int indices = 0;
/* 0x18 = 0001 1000 */
/* 0x9A = 1001 1010 */
if (args.debug)
DEBUG("W: surf=%p reverse=%d flags=(%08X)%c%c%c%c%c%c%c%c type=%s normal=(%f %f %f)",
args.warp, reverse, args.warp->flags,
(args.warp->flags & SURF_PLANEBACK) ? 'B' : '.',
(args.warp->flags & SURF_DRAWSKY) ? 'S' : '.',
(args.warp->flags & SURF_DRAWTURB_QUADS) ? 'Q' : '.',
(args.warp->flags & SURF_DRAWTURB) ? 'U' : '.',
(args.warp->flags & SURF_DRAWTILED) ? 'T' : '.',
(args.warp->flags & SURF_CONVEYOR) ? 'C' : '.',
(args.warp->flags & SURF_UNDERWATER) ? 'W' : '.',
(args.warp->flags & SURF_TRANSPARENT) ? 'A' : '.',
args.warp->plane->type == PLANE_Z ? "Z" :
args.warp->plane->type == PLANE_Y ? "Y" :
args.warp->plane->type == PLANE_X ? "X" :
args.warp->plane->type == PLANE_NONAXIAL ? "N" : "?",
args.warp->plane->normal[0],
args.warp->plane->normal[1],
args.warp->plane->normal[2]
);
for( const glpoly_t *p = args.warp->polys; p; p = p->next ) {
ASSERT(p->numverts <= MAX_WATER_VERTICES);
const float *v;
if( reverse )
v = p->verts[0] + ( p->numverts - 1 ) * VERTEXSIZE;
else
v = p->verts[0];
for( int i = 0; i < p->numverts; i++ )
{
float nv, prev_nv;
if( args.wave_height )
{
nv = r_turbsin[(int)(time * 160.0f + v[1] + v[0]) & 255] + 8.0f;
nv = (r_turbsin[(int)(v[0] * 5.0f + time * 171.0f - v[1]) & 255] + 8.0f ) * 0.8f + nv;
nv = nv * args.wave_height + v[2];
prev_nv = r_turbsin[(int)(args.prev_time * 160.0f + v[1] + v[0]) & 255] + 8.0f;
prev_nv = (r_turbsin[(int)(v[0] * 5.0f + args.prev_time * 171.0f - v[1]) & 255] + 8.0f ) * 0.8f + prev_nv;
prev_nv = prev_nv * args.wave_height + v[2];
}
else
prev_nv = nv = v[2];
const float os = v[3];
const float ot = v[4];
float s = os + r_turbsin[(int)((ot * 0.125f + gpGlobals->time) * TURBSCALE) & 255];
s *= ( 1.0f / SUBDIVIDE_SIZE );
float t = ot + r_turbsin[(int)((os * 0.125f + gpGlobals->time) * TURBSCALE) & 255];
t *= ( 1.0f / SUBDIVIDE_SIZE );
poly_vertices[i].pos[0] = v[0];
poly_vertices[i].pos[1] = v[1];
poly_vertices[i].pos[2] = nv;
poly_vertices[i].prev_pos[0] = v[0];
poly_vertices[i].prev_pos[1] = v[1];
poly_vertices[i].prev_pos[2] = prev_nv;
poly_vertices[i].gl_tc[0] = s;
poly_vertices[i].gl_tc[1] = t;
poly_vertices[i].lm_tc[0] = 0;
poly_vertices[i].lm_tc[1] = 0;
Vector4Set(poly_vertices[i].color, 255, 255, 255, 255);
poly_vertices[i].normal[0] = 0;
poly_vertices[i].normal[1] = 0;
poly_vertices[i].normal[2] = 0;
poly_vertices[i].tangent[0] = 0;
poly_vertices[i].tangent[1] = 0;
poly_vertices[i].tangent[2] = 0;
if (i > 1) {
vec3_t e0, e1, normal, tangent;
VectorSubtract( poly_vertices[i - 1].pos, poly_vertices[0].pos, e0 );
VectorSubtract( poly_vertices[i].pos, poly_vertices[0].pos, e1 );
CrossProduct( e1, e0, normal );
VectorAdd(normal, poly_vertices[0].normal, poly_vertices[0].normal);
VectorAdd(normal, poly_vertices[i].normal, poly_vertices[i].normal);
VectorAdd(normal, poly_vertices[i - 1].normal, poly_vertices[i - 1].normal);
computeTangentE(tangent, e0, e1,
poly_vertices[0].gl_tc, poly_vertices[i-1].gl_tc, poly_vertices[i].gl_tc);
VectorAdd(tangent, poly_vertices[0].tangent, poly_vertices[0].tangent);
VectorAdd(tangent, poly_vertices[i].tangent, poly_vertices[i].tangent);
VectorAdd(tangent, poly_vertices[i - 1].tangent, poly_vertices[i - 1].tangent);
args.dst_indices[indices++] = (uint16_t)(vertices);
args.dst_indices[indices++] = (uint16_t)(vertices + i - 1);
args.dst_indices[indices++] = (uint16_t)(vertices + i);
}
if( reverse )
v -= VERTEXSIZE;
else
v += VERTEXSIZE;
}
for( int i = 0; i < p->numverts; i++ ) {
VectorNormalize(poly_vertices[i].normal);
VectorNormalize(poly_vertices[i].tangent);
#if 0
//const float dot = DotProduct(poly_vertices[i].normal, args.warp->plane->normal);
//if (dot < 0.) {
if (poly_vertices[i].normal[2] < 0.f) {
Vector4Set(poly_vertices[i].color, 255, 0, 0, 255);
poly_vertices[i].pos[0] -= 30.f;
poly_vertices[i].prev_pos[0] -= 30.f;
poly_vertices[i].pos[2] -= 1.f;
poly_vertices[i].prev_pos[2] -= 1.f;
} else {
Vector4Set(poly_vertices[i].color, 0, 255, 0, 255);
poly_vertices[i].pos[0] += 30.f;
poly_vertices[i].prev_pos[0] += 30.f;
poly_vertices[i].pos[2] += 1.f;
poly_vertices[i].prev_pos[2] += 1.f;
}
#endif
}
if (args.debug)
DEBUG(" poly numvers=%d flags=%08X normal=(%f %f %f)",
p->numverts, p->flags,
poly_vertices[0].normal[0],
poly_vertices[0].normal[1],
poly_vertices[0].normal[2]
);
memcpy(args.dst_vertices + vertices, poly_vertices, sizeof(vk_vertex_t) * p->numverts);
vertices += p->numverts;
}
// FIXME VK GL_SetupFogColorForSurfaces();
// Render
const int tex_id = args.warp->texinfo->texture->gl_texturenum;
const r_vk_material_t material = R_VkMaterialGetForTexture(tex_id);
*args.dst_geometry = (vk_render_geometry_t){
.material = material,
.ye_olde_texture = tex_id,
.surf_deprecate = args.warp,
.max_vertex = vertices,
.element_count = indices,
.emissive = {0,0,0},
};
RT_GetEmissiveForTexture(args.dst_geometry->emissive, tex_id);
*args.out_vertex_count = vertices;
*args.out_index_count = indices;
g_brush.stat.water_surfaces_drawn++;
g_brush.stat.water_polys_drawn += indices / 3;
}
static vk_render_type_e brushRenderModeToRenderType( int render_mode ) {
switch (render_mode) {
case kRenderNormal: return kVkRenderTypeSolid;
case kRenderTransColor: return kVkRenderType_A_1mA_RW;
case kRenderTransTexture: return kVkRenderType_A_1mA_R;
case kRenderGlow: return kVkRenderType_A_1mA_R;
case kRenderTransAlpha: return kVkRenderType_AT;
case kRenderTransAdd: return kVkRenderType_A_1_R;
default: ASSERT(!"Unxpected render_mode");
}
return kVkRenderTypeSolid;
}
typedef struct {
const cl_entity_t *ent;
const msurface_t *surfaces;
r_brush_water_model_t *wmodel;
vk_render_geometry_t *geometries;
float prev_time;
qboolean debug;
} fill_water_surfaces_args_t;
static void fillWaterSurfaces( fill_water_surfaces_args_t args ) {
ASSERT(args.wmodel->surfaces_count > 0);
const float wave_height = (!args.ent) ? 0.f : args.ent->curstate.scale;
const r_geometry_range_lock_t geom_lock = R_GeometryRangeLock(&args.wmodel->geometry);
int vertices_offset = 0;
int indices_offset = 0;
for (int i = 0; i < args.wmodel->surfaces_count; ++i) {
const int surf_index = args.wmodel->surfaces_indices[i];
const msurface_t *warp = args.surfaces + surf_index;
int vertices = 0, indices = 0;
brushComputeWaterPolys((compute_water_polys_t){
.prev_time = args.prev_time,
.wave_height = wave_height,
.warp = warp,
.dst_vertices = geom_lock.vertices + vertices_offset,
.dst_indices = geom_lock.indices + indices_offset,
.dst_geometry = args.geometries + i,
.out_vertex_count = &vertices,
.out_index_count = &indices,
.debug = args.debug,
});
args.geometries[i].vertex_offset = args.wmodel->geometry.vertices.unit_offset + vertices_offset;
args.geometries[i].index_offset = args.wmodel->geometry.indices.unit_offset + indices_offset;
vertices_offset += vertices;
indices_offset += indices;
ASSERT(vertices_offset <= args.wmodel->geometry.vertices.count);
ASSERT(indices_offset <= args.wmodel->geometry.indices.count);
}
R_GeometryRangeUnlock( &geom_lock );
}
static qboolean loadPolyLight(rt_light_add_polygon_t *out_polygon, const model_t *mod, const int surface_index, const msurface_t *surf, const vec3_t emissive);
static qboolean doesTextureChainChange( const texture_t *const base ) {
const texture_t *cur = base;
if (!cur)
return false;
cur = cur->anim_next;
while (cur && cur != base) {
if (cur->gl_texturenum != base->gl_texturenum)
return true;
cur = cur->anim_next;
}
return false;
}
static qboolean isSurfaceAnimated( const msurface_t *s, qboolean is_worldmodel ) {
const texture_t *const base = s->texinfo->texture;
if( !base->anim_total && !base->alternate_anims )
return false;
/* TODO why did we do this? It doesn't seem to rule out animation really.
if( base->name[0] == '-' )
return false;
*/
// Worldmodel cannot be triggered and change between alternate_anims and regular anims,
// therefore it should not be checked. There are lights (e.g. in c2a5) which have alternate anims.
// These lights get incorrectly marked as dynamic, tanking the performance.
if (!is_worldmodel && base->alternate_anims && base->gl_texturenum != base->alternate_anims->gl_texturenum)
return true;
return doesTextureChainChange(base) || (!is_worldmodel && doesTextureChainChange(base->alternate_anims));
}
typedef enum {
BrushSurface_Hidden = 0,
BrushSurface_Regular,
BrushSurface_Animated,
BrushSurface_Water,
BrushSurface_WaterSide,
BrushSurface_Sky,
BrushSurface_Conveyor,
} brush_surface_type_e;
static brush_surface_type_e getSurfaceType( const msurface_t *surf, int i, qboolean is_worldmodel ) {
// if ( i >= 0 && (surf->flags & ~(SURF_PLANEBACK | SURF_UNDERWATER | SURF_TRANSPARENT)) != 0)
// {
// DEBUG("\t%d flags: ", i);
// #define PRINTFLAGS(X) \
// X(SURF_PLANEBACK) \
// X(SURF_DRAWSKY) \
// X(SURF_DRAWTURB_QUADS) \
// X(SURF_DRAWTURB) \
// X(SURF_DRAWTILED) \
// X(SURF_CONVEYOR) \
// X(SURF_UNDERWATER) \
// X(SURF_TRANSPARENT)
// #define PRINTFLAG(f) if (FBitSet(surf->flags, f)) DEBUG(" %s", #f);
// PRINTFLAGS(PRINTFLAG)
// DEBUG("\n");
// }
const xvk_patch_surface_t *patch_surface = R_VkPatchGetSurface(i);
if (patch_surface && patch_surface->flags & Patch_Surface_Delete)
return BrushSurface_Hidden;
if (surf->flags & (SURF_DRAWTURB | SURF_DRAWTURB_QUADS)) {
if (!surf->polys)
return BrushSurface_Hidden;
// Water surfaces come in pairs: regular front and the opposite back
// This makes ray tracing unhappy as there are coplanar surfaces.
// We'd want to turn of the back surface, but SURF_PLANEBACK is not really congruent with
// the logical direction of the surface, it just means that glpolys have been produced in
// an opposite winding order.
// SURF_UNDERWATER seems to be the right flag: it does seem to signal that the surface is
// lookint "out" from the water, directed towards "air".
if (surf->flags & SURF_UNDERWATER)
return BrushSurface_Hidden;
//}
// Worldmodel doesn't distinguish between !=PLANE_Z sides and not sides.
// All water surfaces should be present for worldmodel
return (is_worldmodel || surf->plane->type == PLANE_Z) ? BrushSurface_Water : BrushSurface_WaterSide;
}
// Explicitly enable SURF_SKY, otherwise they will be skipped by SURF_DRAWTILED
if( FBitSet( surf->flags, SURF_DRAWSKY ))
return BrushSurface_Sky;
if( surf->flags & SURF_CONVEYOR ) {
return BrushSurface_Conveyor;
}
//if( surf->flags & ( SURF_DRAWSKY | SURF_DRAWTURB | SURF_CONVEYOR | SURF_DRAWTURB_QUADS ) ) {
if( surf->flags & ( SURF_DRAWTURB | SURF_DRAWTURB_QUADS ) ) {
// FIXME don't print this on second sort-by-texture pass
//DEBUG("Skipping surface %d because of flags %08x", i, surf->flags);
return BrushSurface_Hidden;
}
if( FBitSet( surf->flags, SURF_DRAWTILED )) {
//DEBUG("Skipping surface %d because of tiled flag", i);
return BrushSurface_Hidden;
}
const qboolean patched_material = patch_surface && !!(patch_surface->flags & Patch_Surface_Material);
if (!patched_material && isSurfaceAnimated(surf, is_worldmodel)) {
return BrushSurface_Animated;
}
return BrushSurface_Regular;
}
static qboolean brushCreateWaterModel(const model_t *mod, r_brush_water_model_t *wmodel, const water_model_sizes_t sizes, brush_surface_type_e type, qboolean is_worldmodel) {
const r_geometry_range_t geometry = R_GeometryRangeAlloc(sizes.vertices, sizes.indices);
if (!geometry.block_handle.size) {
ERR("Cannot allocate geometry (v=%d, i=%d) for water model %s",
sizes.vertices, sizes.indices, mod->name );
return false;
}
vk_render_geometry_t *const geometries = Mem_Malloc(vk_core.pool, sizeof(vk_render_geometry_t) * sizes.surfaces);
int* const surfaces_indices = Mem_Malloc(vk_core.pool, sizes.surfaces * sizeof(int));
int surfaces_count = 0;
for( int i = 0; i < mod->nummodelsurfaces; ++i) {
const int surface_index = mod->firstmodelsurface + i;
const msurface_t *surf = mod->surfaces + surface_index;
if (getSurfaceType(surf, surface_index, is_worldmodel) == type) {
surfaces_indices[surfaces_count++] = surface_index;
}
}
ASSERT(surfaces_count == sizes.surfaces);
wmodel->surfaces_indices = surfaces_indices;
wmodel->surfaces_count = surfaces_count;
wmodel->surfaces_indices = surfaces_indices;
wmodel->geometry = geometry;
fillWaterSurfaces( (fill_water_surfaces_args_t){
.ent = NULL,
.surfaces = mod->surfaces,
.wmodel = wmodel,
.geometries = geometries,
.prev_time = 0.f,
.debug = true,
});
if (!R_RenderModelCreate(&wmodel->render_model, (vk_render_model_init_t){
.name = mod->name,
.geometries = geometries,
.geometries_count = surfaces_count,
.dynamic = true,
})) {
ERR("Could not create water render model for brush model %s", mod->name);
return false;
}
return true;
}
static material_mode_e brushMaterialModeForRenderType(vk_render_type_e render_type) {
switch (render_type) {
case kVkRenderTypeSolid:
return kMaterialMode_Opaque;
break;
case kVkRenderType_A_1mA_RW: // blend: scr*a + dst*(1-a), depth: RW
case kVkRenderType_A_1mA_R: // blend: scr*a + dst*(1-a), depth test
return kMaterialMode_Translucent;
break;
case kVkRenderType_A_1: // blend: scr*a + dst, no depth test or write; sprite:kRenderGlow only
return kMaterialMode_BlendGlow;
break;
case kVkRenderType_A_1_R: // blend: scr*a + dst, depth test
case kVkRenderType_1_1_R: // blend: scr + dst, depth test
return kMaterialMode_BlendAdd;
break;
case kVkRenderType_AT: // no blend, depth RW, alpha test
return kMaterialMode_AlphaTest;
break;
default:
gEngine.Host_Error("Unexpected render type %d\n", render_type);
}
return kMaterialMode_Opaque;
}
static void brushDrawWater(r_brush_water_model_t *wmodel, const cl_entity_t *ent, const msurface_t *surfaces, int render_type, const vec4_t color, const matrix4x4 transform, const matrix4x4 prev_transform, float prev_time) {
APROF_SCOPE_DECLARE_BEGIN(brush_draw_water, __FUNCTION__);
ASSERT(wmodel->surfaces_count > 0);
fillWaterSurfaces((fill_water_surfaces_args_t){
.ent = ent,
.surfaces = surfaces,
.wmodel = wmodel,
.geometries = wmodel->render_model.geometries,
.prev_time = prev_time,
.debug = false,
});
if (!R_RenderModelUpdate(&wmodel->render_model)) {
ERR("Failed to update brush model \"%s\" water", wmodel->render_model.debug_name);
}
const material_mode_e material_mode = brushMaterialModeForRenderType(render_type);
R_RenderModelDraw(&wmodel->render_model, (r_model_draw_t){
.render_type = render_type,
.material_mode = material_mode,
.material_flags = kMaterialFlag_None,
.color = (const vec4_t*)color,
.transform = (const matrix4x4*)transform,
.prev_transform = (const matrix4x4*)prev_transform,
.override = {
.material = NULL,
.old_texture = -1,
},
});
APROF_SCOPE_END(brush_draw_water);
}
static void computeConveyorOffset(const color24 rendercolor, float tex_width, float time, vec2_t out_offset) {
float sy, cy;
float flConveyorSpeed = 0.0f;
float flRate, flAngle;
// TODO
/* if( ENGINE_GET_PARM( PARM_QUAKE_COMPATIBLE ) && RI.currententity == gEngfuncs.GetEntityByIndex( 0 ) ) */
/* { */
/* // same as doom speed */
/* flConveyorSpeed = -35.0f; */
/* } */
/* else */
{
flConveyorSpeed = (rendercolor.g<<8|rendercolor.b) / 16.0f;
if( rendercolor.r ) flConveyorSpeed = -flConveyorSpeed;
}
flRate = fabs( flConveyorSpeed ) / tex_width;
flAngle = ( flConveyorSpeed >= 0 ) ? 180 : 0;
// TODO no SinCos, no
SinCos( flAngle * ( M_PI_F / 180.0f ), &sy, &cy );
out_offset[0] = cy * flRate * time;
out_offset[1] = sy * flRate * time;
// make sure that we are positive
if( out_offset[0] < 0.0f ) out_offset[0] += 1.0f + -(int)out_offset[0];
if( out_offset[1] < 0.0f ) out_offset[1] += 1.0f + -(int)out_offset[1];
// make sure that we are in a [0,1] range
out_offset[0] = out_offset[0] - (int)out_offset[0];
out_offset[1] = out_offset[1] - (int)out_offset[1];
}
/*
===============
R_TextureAnimation
Returns the proper texture for a given time and surface
===============
*/
const texture_t *R_TextureAnimation( const cl_entity_t *ent, const msurface_t *s )
{
const texture_t *base = s->texinfo->texture;
int count, reletive;
if( ent && ent->curstate.frame )
{
if( base->alternate_anims )
base = base->alternate_anims;
}
if( !base->anim_total )
return base;
if( base->name[0] == '-' )
{
int tx = (int)((s->texturemins[0] + (base->width << 16)) / base->width) % MOD_FRAMES;
int ty = (int)((s->texturemins[1] + (base->height << 16)) / base->height) % MOD_FRAMES;
reletive = g_brush.rtable[tx][ty] % base->anim_total;
}
else
{
int speed;
// Quake1 textures uses 10 frames per second
/* TODO
if( FBitSet( R_TextureGetByIndex( base->gl_texturenum )->flags, TF_QUAKEPAL ))
speed = 10;
else */ speed = 20;
reletive = (int)(gpGlobals->time * speed) % base->anim_total;
}
count = 0;
while( base->anim_min > reletive || base->anim_max <= reletive )
{
base = base->anim_next;
if( !base || ++count > MOD_FRAMES )
return s->texinfo->texture;
}
return base;
}
void R_BrushModelDraw( const cl_entity_t *ent, int render_mode, float blend, const matrix4x4 in_transform ) {
// Expect all buffers to be bound
const model_t *mod = ent->model;
vk_brush_model_t *bmodel = mod->cache.data;
if (!bmodel) {
ERR("Model %s wasn't loaded", mod->name);
return;
}
matrix4x4 transform;
if (in_transform)
Matrix4x4_Copy(transform, in_transform);
else
Matrix4x4_LoadIdentity(transform);
if (bmodel->patch_rendermode >= 0)
render_mode = bmodel->patch_rendermode;
// Add dynamic polylights if any
for (int i = 0; i < bmodel->dynamic_polylights_count; ++i) {
rt_light_add_polygon_t *const polylight = bmodel->dynamic_polylights + i;
polylight->transform_row = (const matrix3x4*)transform;
polylight->dynamic = true;
RT_LightAddPolygon(polylight);
}
vec4_t color = {1, 1, 1, 1};
vk_render_type_e render_type = kVkRenderTypeSolid;
uint32_t material_flags = kMaterialFlag_None;
switch (render_mode) {
case kRenderNormal:
Vector4Set(color, 1.f, 1.f, 1.f, 1.f);
render_type = kVkRenderTypeSolid;
break;
case kRenderTransColor:
render_type = kVkRenderType_A_1mA_RW;
Vector4Set(color,
ent->curstate.rendercolor.r / 255.f,
ent->curstate.rendercolor.g / 255.f,
ent->curstate.rendercolor.b / 255.f,
blend);
break;
case kRenderTransAdd:
Vector4Set(color, blend, blend, blend, 1.f);
render_type = kVkRenderType_A_1_R;
material_flags |= kMaterialFlag_CullBackFace_Bit;
break;
case kRenderTransAlpha:
if( gEngine.EngineGetParm( PARM_QUAKE_COMPATIBLE, 0 ))
{
render_type = kVkRenderType_A_1mA_RW;
Vector4Set(color, 1.f, 1.f, 1.f, blend);
}
else
{
Vector4Set(color, 1.f, 1.f, 1.f, 1.f);
render_type = kVkRenderType_AT;
}
break;
case kRenderTransTexture:
case kRenderGlow:
render_type = kVkRenderType_A_1mA_R;
Vector4Set(color, 1.f, 1.f, 1.f, blend);
break;
}
// Only Normal and TransAlpha have lightmaps
// TODO: on big maps more than a single lightmap texture is possible
bmodel->render_model.lightmap = (render_mode == kRenderNormal || render_mode == kRenderTransAlpha) ? 1 : 0;
if (bmodel->water.surfaces_count)
brushDrawWater(&bmodel->water, ent, bmodel->engine_model->surfaces, render_type, color, transform, bmodel->prev_transform, bmodel->prev_time);
if (bmodel->water_sides.surfaces_count && FBitSet( ent->curstate.effects, EF_WATERSIDES ) ) {
brushDrawWater(&bmodel->water_sides, ent, bmodel->engine_model->surfaces, render_type, color, transform, bmodel->prev_transform, bmodel->prev_time);
}
++g_brush.stat.models_drawn;
if (bmodel->render_model.num_geometries == 0)
return;
// Animate textures
{
APROF_SCOPE_DECLARE_BEGIN(brush_update_textures, "brush: update animated textures");
// Update animated textures
int updated_textures_count = 0;
for (int i = 0; i < bmodel->animated_indexes_count; ++i) {
const int geom_index = bmodel->animated_indexes[i];
vk_render_geometry_t *geom = bmodel->render_model.geometries + geom_index;
const int surface_index = geom->surf_deprecate - mod->surfaces;
// Optionally patch by texture_s pointer and run animations
const texture_t *t = R_TextureAnimation(ent, geom->surf_deprecate);
const int new_tex_id = t->gl_texturenum;
ASSERT(new_tex_id >= 0);
// Animated textures can be emissive
// Add them as dynamic lights for now. It would probably be better if they were static lights (for worldmodel),
// but there's no easy way to do it for now.
vec3_t *emissive = &bmodel->render_model.geometries[geom_index].emissive;
if (RT_GetEmissiveForTexture(*emissive, new_tex_id)) {
rt_light_add_polygon_t polylight;
if (loadPolyLight(&polylight, mod, surface_index, geom->surf_deprecate, *emissive)) {
polylight.dynamic = true;
polylight.transform_row = (const matrix3x4*)&transform;
RT_LightAddPolygon(&polylight);
}
}
if (new_tex_id == geom->ye_olde_texture)
continue;
geom->ye_olde_texture = new_tex_id;
geom->material = R_VkMaterialGetForTexture(new_tex_id);
if (updated_textures_count < MAX_ANIMATED_TEXTURES) {
g_brush.updated_textures[updated_textures_count++] = bmodel->animated_indexes[i];
}
}
if (updated_textures_count > 0) {
R_RenderModelUpdateMaterials(&bmodel->render_model, g_brush.updated_textures, updated_textures_count);
}
APROF_SCOPE_END(brush_update_textures);
}
// Move conveyors
for (int i = 0; i < bmodel->conveyors_count; ++i) {
const r_conveyor_t *const conv = bmodel->conveyors + i;
vec2_t offset = {0, 0};
computeConveyorOffset(ent->curstate.rendercolor, conv->texture_width, gpGlobals->time, offset);
ASSERT(conv->geometry_index >= 0);
ASSERT(conv->geometry_index < bmodel->render_model.num_geometries);
const vk_render_geometry_t *const geom = bmodel->render_model.geometries + conv->geometry_index;
const r_geometry_range_lock_t lock = R_GeometryRangeLockSubrange(&bmodel->geometry, conv->vertices_dst_offset, conv->vertices_count);
for (int j = 0; j < conv->vertices_count; ++j) {
const vk_vertex_t *const src = bmodel->conveyors_vertices + conv->vertices_src_offset + j;
vk_vertex_t *const dst = lock.vertices + j;
*dst = *src;
dst->gl_tc[0] = src->gl_tc[0] + offset[0];
dst->gl_tc[1] = src->gl_tc[1] + offset[1];
}
R_GeometryRangeUnlock(&lock);
}
const material_mode_e material_mode = brushMaterialModeForRenderType(render_type);
R_RenderModelDraw(&bmodel->render_model, (r_model_draw_t){
.render_type = render_type,
.material_mode = material_mode,
.material_flags = material_flags,
.color = &color,
.transform = &transform,
.prev_transform = &bmodel->prev_transform,
.override = {
.material = NULL,
.old_texture = -1,
},
});
Matrix4x4_Copy(bmodel->prev_transform, transform);
bmodel->prev_time = gpGlobals->time;
}
static model_sizes_t computeSizes( const model_t *mod, qboolean is_worldmodel ) {
model_sizes_t sizes = {0};
for( int i = 0; i < mod->nummodelsurfaces; ++i)
{
const int surface_index = mod->firstmodelsurface + i;
const msurface_t *surf = mod->surfaces + surface_index;
const int tex_id = surf->texinfo->texture->gl_texturenum;
if (tex_id > sizes.max_texture_id)
sizes.max_texture_id = tex_id;
switch (getSurfaceType(surf, surface_index, is_worldmodel)) {
case BrushSurface_Water:
sizes.water.surfaces++;
addWarpVertIndCounts(surf, &sizes.water.vertices, &sizes.water.indices);
continue;
case BrushSurface_WaterSide:
sizes.side_water.surfaces++;
addWarpVertIndCounts(surf, &sizes.side_water.vertices, &sizes.side_water.indices);
continue;
case BrushSurface_Hidden:
continue;
case BrushSurface_Animated:
sizes.animated_count++;
break;
case BrushSurface_Conveyor:
sizes.conveyors_count++;
sizes.conveyors_vertices_count += surf->numedges;
break;
case BrushSurface_Sky:
sizes.sky_surfaces_count++;
// Do not count towards surfaces that we'll load (still need to count if for the purpose of loading skybox)
if (g_map_entities.remove_all_sky_surfaces)
continue;
break;
case BrushSurface_Regular:
break;
}
++sizes.num_surfaces;
sizes.num_vertices += surf->numedges;
sizes.num_indices += 3 * (surf->numedges - 1);
}
DEBUG("Computed sizes for brush model \"%s\":", mod->name);
DEBUG(" num_surfaces=%d animated_count=%d num_vertices=%d num_indices=%d max_texture_id=%d",
sizes.num_surfaces, sizes.animated_count, sizes.num_vertices, sizes.num_indices, sizes.max_texture_id);
DEBUG(" conveyors_count=%d conveyors_vertices_count=%d",
sizes.conveyors_count, sizes.conveyors_vertices_count);
DEBUG(" water_surfaces=%d water_vertices=%d water_indices=%d",
sizes.water.surfaces, sizes.water.vertices, sizes.water.indices);
DEBUG(" side_water_surfaces=%d side_water_vertices=%d side_water_indices=%d",
sizes.side_water.surfaces, sizes.side_water.vertices, sizes.side_water.indices);
return sizes;
}
typedef struct {
const model_t *mod;
vk_brush_model_t *bmodel;
model_sizes_t sizes;
uint32_t base_vertex_offset;
uint32_t base_index_offset;
vk_render_geometry_t *out_geometries;
vk_vertex_t *out_vertices;
uint16_t *out_indices;
qboolean is_worldmodel;
} fill_geometries_args_t;
static void getSurfaceNormal( const msurface_t *surf, vec3_t out_normal) {
if( FBitSet( surf->flags, SURF_PLANEBACK ))
VectorNegate( surf->plane->normal, out_normal );
else
VectorCopy( surf->plane->normal, out_normal );
// TODO scale normal by area -- bigger surfaces should have bigger impact
// NOTE scaling normal by area might be totally incorrect in many circumstances
// The more corect logic there is way more difficult
//VectorScale(normal, surf->plane.
}
static qboolean shouldSmoothLinkSurfaces(const model_t* mod, qboolean smooth_entire_model, int surf1, int surf2) {
// Filter explicit exclusion
for (int i = 0; i < g_map_entities.smoothing.excluded_pairs_count; i+=2) {
const int cand1 = g_map_entities.smoothing.excluded_pairs[i];
const int cand2 = g_map_entities.smoothing.excluded_pairs[i+1];
if ((cand1 == surf1 && cand2 == surf2)
|| (cand1 == surf2 && cand2 == surf1))
return false;
}
qboolean excluded = false;
for (int i = 0; i < g_map_entities.smoothing.excluded_count; ++i) {
const int cand = g_map_entities.smoothing.excluded[i];
if (cand == surf1 || cand == surf2) {
excluded = true;
break;
}
}
if (smooth_entire_model && !excluded)
return true;
// Smoothing groups have priority over individual exclusion.
// That way we can exclude a surface from smoothing with most of its neighbours,
// but still smooth it with some.
for (int i = 0; i < g_map_entities.smoothing.groups_count; ++i) {
const xvk_smoothing_group_t *g = g_map_entities.smoothing.groups + i;
uint32_t bits = 0;
for (int j = 0; j < g->count; ++j) {
if (g->surfaces[j] == surf1) {
bits |= 1;
if (bits == 3)
return true;
}
else if (g->surfaces[j] == surf2) {
bits |= 2;
if (bits == 3)
return true;
}
}
}
if (excluded)
return false;
// Do not join surfaces with different textures. Assume they belong to different objects.
{
// Should we also check texture/material patches too to filter out pairs which originally had
// same textures, but with patches do not?
if (mod->surfaces[surf1].texinfo->texture->gl_texturenum
!= mod->surfaces[surf2].texinfo->texture->gl_texturenum)
return false;
}
vec3_t n1, n2;
getSurfaceNormal(mod->surfaces + surf1, n1);
getSurfaceNormal(mod->surfaces + surf2, n2);
const float dot = DotProduct(n1, n2);
// TODO smooth verbose group DEBUG("Smoothing: dot(%d, %d) = %f (t=%f)", surf1, surf2, dot, g_map_entities.smoothing.threshold);
return dot >= g_map_entities.smoothing.threshold;
}
static int lvFindValue(const linked_value_t *li, int count, int needle) {
for (int i = 0; i < count; ++i)
if (li[i].value == needle)
return i;
return -1;
}
static int lvFindOrAddValue(linked_value_t *li, int *count, int capacity, int needle) {
const int found = lvFindValue(li, *count, needle);
if (found >= 0)
return found;
if (*count == capacity)
return -1;
li[*count].value = needle;
li[*count].link = *count;
return (*count)++;
}
static int lvFindBaseIndex(const linked_value_t *li, int index) {
while (li[index].link != index)
index = li[index].link;
return index;
}
static void lvFlatten(linked_value_t *li, int count) {
for (int i = 0; i < count; ++i) {
for (int j = i; j < count; ++j) {
if (lvFindBaseIndex(li, j) == i) {
li[j].link = i;
}
}
}
}
static void linkSmoothSurfaces(const model_t* mod, int surf1, int surf2, int vertex_index) {
conn_vertex_t *v = g_brush.conn.vertices + vertex_index;
int i1 = lvFindOrAddValue(v->surfs, &v->count, COUNTOF(v->surfs), surf1);
int i2 = lvFindOrAddValue(v->surfs, &v->count, COUNTOF(v->surfs), surf2);
// TODO smooth_verbose DEBUG("Link %d(%d)<->%d(%d) v=%d", surf1, i1, surf2, i2, vertex_index);
if (i1 < 0 || i2 < 0) {
ERR("Model %s cannot smooth link surf %d<->%d for vertex %d", mod->name, surf1, surf2, vertex_index);
return;
}
i1 = lvFindBaseIndex(v->surfs, i1);
i2 = lvFindBaseIndex(v->surfs, i2);
// Link them
v->surfs[Q_max(i1, i2)].link = Q_min(i1, i2);
}
static void connectVertices( const model_t *mod, qboolean smooth_entire_model ) {
if (mod->numedges > g_brush.conn.edges_capacity) {
if (g_brush.conn.edges)
Mem_Free(g_brush.conn.edges);
g_brush.conn.edges_capacity = mod->numedges;
g_brush.conn.edges = Mem_Calloc(vk_core.pool, sizeof(*g_brush.conn.edges) * g_brush.conn.edges_capacity);
}
if (mod->numvertexes > g_brush.conn.vertices_capacity) {
if (g_brush.conn.vertices)
Mem_Free(g_brush.conn.vertices);
g_brush.conn.vertices_capacity = mod->numvertexes;
g_brush.conn.vertices = Mem_Calloc(vk_core.pool, sizeof(*g_brush.conn.vertices) * g_brush.conn.vertices_capacity);
}
// Find connection edges
for (int i = 0; i < mod->nummodelsurfaces; ++i) {
const int surface_index = mod->firstmodelsurface + i;
const msurface_t *surf = mod->surfaces + surface_index;
for(int k = 0; k < surf->numedges; k++) {
const int iedge_dir = mod->surfedges[surf->firstedge + k];
const int iedge = iedge_dir >= 0 ? iedge_dir : -iedge_dir;
ASSERT(iedge >= 0);
ASSERT(iedge < mod->numedges);
conn_edge_t *cedge = g_brush.conn.edges + iedge;
if (cedge->count == 0) {
cedge->first_surface = surface_index;
} else {
const medge_t *edge = mod->edges + iedge;
if (shouldSmoothLinkSurfaces(mod, smooth_entire_model, cedge->first_surface, surface_index)) {
linkSmoothSurfaces(mod, cedge->first_surface, surface_index, edge->v[0]);
linkSmoothSurfaces(mod, cedge->first_surface, surface_index, edge->v[1]);
}
if (cedge->count > 1) {
WARN("Model %s edge %d has %d surfaces", mod->name, i, cedge->count);
}
}
cedge->count++;
} // for surf->numedges
} // for mod->nummodelsurfaces
int hist[17] = {0};
for (int i = 0; i < mod->numvertexes; ++i) {
conn_vertex_t *vtx = g_brush.conn.vertices + i;
if (vtx->count < 16) {
hist[vtx->count]++;
} else {
hist[16]++;
}
lvFlatten(vtx->surfs, vtx->count);
// Too verbose
#if 0
if (vtx->count) {
DEBUG("Vertex %d linked count %d", i, vtx->count);
for (int j = 0; j < vtx->count; ++j) {
DEBUG(" %d: l=%d v=%d", j, vtx->surfs[j].link, vtx->surfs[j].value);
}
}
#endif
}
/* TODO smooth_debug
for (int i = 0; i < COUNTOF(hist); ++i) {
DEBUG("VTX hist[%d] = %d", i, hist[i]);
}
*/
}
static qboolean getSmoothedNormalFor(const model_t* mod, int vertex_index, int surface_index, vec3_t out_normal) {
const conn_vertex_t *v = g_brush.conn.vertices + vertex_index;
const int index = lvFindValue(v->surfs, v->count, surface_index);
if (index < 0)
return false;
const int base = lvFindBaseIndex(v->surfs, index);
vec3_t normal = {0};
for (int i = 0; i < v->count; ++i) {
if (v->surfs[i].link == base) {
const int surface = v->surfs[i].value;
vec3_t surf_normal = {0};
getSurfaceNormal(mod->surfaces + surface, surf_normal);
VectorAdd(normal, surf_normal, normal);
}
}
VectorNormalize(normal);
VectorCopy(normal, out_normal);
return true;
}
static const xvk_mapent_func_any_t *getModelFuncAnyPatch( const model_t *const mod ) {
for (int i = 0; i < g_map_entities.func_any_count; ++i) {
const xvk_mapent_func_any_t *const fw = g_map_entities.func_any + i;
if (Q_strcmp(mod->name, fw->model) == 0) {
return fw;
}
}
return NULL;
}
static qboolean fillBrushSurfaces(fill_geometries_args_t args) {
int vertex_offset = 0;
int num_geometries = 0;
int animated_count = 0;
int conveyors_count = 0;
int conveyors_vertices_count = 0;
vk_vertex_t *p_vert = args.out_vertices;
uint16_t *p_ind = args.out_indices;
int index_offset = args.base_index_offset;
const xvk_mapent_func_any_t *const entity_patch = getModelFuncAnyPatch(args.mod);
if (entity_patch) {
DEBUG("Found entity_patch(matmap_count=%d, rendermode_patched=%d rendermode=%d) for model \"%s\"",
entity_patch->matmap_count, entity_patch->rendermode_patched, entity_patch->rendermode, args.mod->name);
if (entity_patch->rendermode_patched > 0)
args.bmodel->patch_rendermode = entity_patch->rendermode;
}
connectVertices(args.mod, entity_patch ? entity_patch->smooth_entire_model : false);
// Load sorted by gl_texturenum
// TODO this does not make that much sense in vulkan (can sort later)
for (int t = 0; t <= args.sizes.max_texture_id; ++t) {
for( int i = 0; i < args.mod->nummodelsurfaces; ++i) {
const int surface_index = args.mod->firstmodelsurface + i;
msurface_t *surf = args.mod->surfaces + surface_index;
const mextrasurf_t *info = surf->info;
vk_render_geometry_t *model_geometry = args.out_geometries + num_geometries;
const float sample_size = gEngine.Mod_SampleSizeForFace( surf );
int index_count = 0;
vec3_t tangent;
const int orig_tex_id = surf->texinfo->texture->gl_texturenum;
if (t != orig_tex_id)
continue;
int tex_id = orig_tex_id;
// TODO this patching should probably override entity patching below
const xvk_patch_surface_t *const psurf = R_VkPatchGetSurface(surface_index);
const brush_surface_type_e type = getSurfaceType(surf, surface_index, args.is_worldmodel);
switch (type) {
case BrushSurface_Water:
case BrushSurface_WaterSide:
case BrushSurface_Hidden:
continue;
case BrushSurface_Animated:
args.bmodel->animated_indexes[animated_count++] = num_geometries;
break;
case BrushSurface_Conveyor:
break;
case BrushSurface_Sky:
if (g_map_entities.remove_all_sky_surfaces)
continue;
case BrushSurface_Regular:
break;
}
args.bmodel->surface_to_geometry_index[i] = num_geometries;
// Fill conveyor data if conveyor
r_conveyor_t *conv = NULL;
if (type == BrushSurface_Conveyor) {
ASSERT(conveyors_count < args.sizes.conveyors_count);
conv = &args.bmodel->conveyors[conveyors_count++];
conv->vertices_count = surf->numedges;
conv->vertices_dst_offset = vertex_offset;
conv->vertices_src_offset = conveyors_vertices_count;
conveyors_vertices_count += conv->vertices_count;
ASSERT(conveyors_vertices_count <= args.sizes.conveyors_vertices_count);
conv->geometry_index = num_geometries;
conv->texture_width = R_TexturesGetParm(PARM_TEX_WIDTH, orig_tex_id);
}
++num_geometries;
//DEBUG( "surface %d: numverts=%d numedges=%d", i, surf->polys ? surf->polys->numverts : -1, surf->numedges );
if (vertex_offset + surf->numedges >= UINT16_MAX) {
// We might be able to handle it by adjusting base_vertex_offset, etc
ERR("Model %s indices don't fit into 16 bits", args.mod->name);
return false;
}
model_geometry->ye_olde_texture = orig_tex_id;
qboolean material_assigned = false;
if (psurf && (psurf->flags & Patch_Surface_Material)) {
model_geometry->material = R_VkMaterialGetForRef(psurf->material_ref);
material_assigned = true;
}
if (!material_assigned && entity_patch) {
for (int i = 0; i < entity_patch->matmap_count; ++i) {
if (entity_patch->matmap[i].from_tex == orig_tex_id) {
model_geometry->material = R_VkMaterialGetForRef(entity_patch->matmap[i].to_mat);
DEBUG(" Assigning entity_patch/material[%d] for surf=%d to mat ref=%d",
i, surface_index, entity_patch->matmap[i].to_mat.index);
material_assigned = true;
break;
}
}
if (!material_assigned && entity_patch->rendermode > 0) {
material_assigned = R_VkMaterialGetEx(tex_id, entity_patch->rendermode, &model_geometry->material);
if (!material_assigned && entity_patch->rendermode == kRenderTransColor) {
// TransColor means ignore textures and draw just color
model_geometry->material = R_VkMaterialGetForTexture(tglob.whiteTexture);
model_geometry->ye_olde_texture = tglob.whiteTexture;
material_assigned = true;
}
}
}
if (!material_assigned) {
model_geometry->material = R_VkMaterialGetForTexture(tex_id);
material_assigned = true;
}
// Make sure animated textures undergo at least the first update
// To update emissive and other texture states
if (type == BrushSurface_Animated)
model_geometry->ye_olde_texture = -1;
VectorClear(model_geometry->emissive);
model_geometry->surf_deprecate = surf;
model_geometry->vertex_offset = args.base_vertex_offset;
model_geometry->max_vertex = vertex_offset + surf->numedges;
model_geometry->index_offset = index_offset;
if ( type == BrushSurface_Sky ) {
model_geometry->material.tex_base_color = TEX_BASE_SKYBOX;
model_geometry->ye_olde_texture = TEX_BASE_SKYBOX;
} else {
ASSERT(!FBitSet( surf->flags, SURF_DRAWTILED ));
VK_CreateSurfaceLightmap( surf, args.mod );
}
vec3_t surf_normal;
getSurfaceNormal(surf, surf_normal);
vk_vertex_t *const pvert_begin = p_vert;
vec3_t p[3];
for( int k = 0; k < surf->numedges; k++ )
{
const int iedge_dir = args.mod->surfedges[surf->firstedge + k];
const int iedge = iedge_dir >= 0 ? iedge_dir : -iedge_dir;
const medge_t *edge = args.mod->edges + iedge;
const int vertex_index = iedge_dir >= 0 ? edge->v[0] : edge->v[1];
const mvertex_t *in_vertex = args.mod->vertexes + vertex_index;
vk_vertex_t vertex = {
.pos = {in_vertex->position[0], in_vertex->position[1], in_vertex->position[2]},
};
vertex.prev_pos[0] = in_vertex->position[0];
vertex.prev_pos[1] = in_vertex->position[1];
vertex.prev_pos[2] = in_vertex->position[2];
// Compute texture coordinates, process tangent
{
vec4_t svec, tvec;
if (psurf && (psurf->flags & Patch_Surface_TexMatrix)) {
svec[0] = surf->texinfo->vecs[0][0] * psurf->texmat_s[0] + surf->texinfo->vecs[1][0] * psurf->texmat_s[1];
svec[1] = surf->texinfo->vecs[0][1] * psurf->texmat_s[0] + surf->texinfo->vecs[1][1] * psurf->texmat_s[1];
svec[2] = surf->texinfo->vecs[0][2] * psurf->texmat_s[0] + surf->texinfo->vecs[1][2] * psurf->texmat_s[1];
svec[3] = surf->texinfo->vecs[0][3] + psurf->texmat_s[2];
tvec[0] = surf->texinfo->vecs[0][0] * psurf->texmat_t[0] + surf->texinfo->vecs[1][0] * psurf->texmat_t[1];
tvec[1] = surf->texinfo->vecs[0][1] * psurf->texmat_t[0] + surf->texinfo->vecs[1][1] * psurf->texmat_t[1];
tvec[2] = surf->texinfo->vecs[0][2] * psurf->texmat_t[0] + surf->texinfo->vecs[1][2] * psurf->texmat_t[1];
tvec[3] = surf->texinfo->vecs[1][3] + psurf->texmat_t[2];
} else {
Vector4Copy(surf->texinfo->vecs[0], svec);
Vector4Copy(surf->texinfo->vecs[1], tvec);
}
const float s = DotProduct( in_vertex->position, svec ) + svec[3];
const float t = DotProduct( in_vertex->position, tvec ) + tvec[3];
vertex.gl_tc[0] = s / surf->texinfo->texture->width;
vertex.gl_tc[1] = t / surf->texinfo->texture->height;
VectorCopy(svec, tangent);
VectorNormalize(tangent);
// "Inverted" texture mapping should not lead to inverted tangent/normal map
// Make sure that orientation is preserved.
{
vec4_t stnorm;
CrossProduct(tvec, svec, stnorm);
if (DotProduct(stnorm, surf_normal) < 0.)
VectorNegate(tangent, tangent);
}
}
// lightmap texture coordinates
{
float s = DotProduct( in_vertex->position, info->lmvecs[0] ) + info->lmvecs[0][3];
s -= info->lightmapmins[0];
s += surf->light_s * sample_size;
s += sample_size * 0.5f;
s /= BLOCK_SIZE * sample_size; //fa->texinfo->texture->width;
float t = DotProduct( in_vertex->position, info->lmvecs[1] ) + info->lmvecs[1][3];
t -= info->lightmapmins[1];
t += surf->light_t * sample_size;
t += sample_size * 0.5f;
t /= BLOCK_SIZE * sample_size; //fa->texinfo->texture->height;
vertex.lm_tc[0] = s;
vertex.lm_tc[1] = t;
}
// Compute smoothed normal if needed
if (!getSmoothedNormalFor(args.mod, vertex_index, surface_index, vertex.normal)) {
VectorCopy(surf_normal, vertex.normal);
}
{
const float normal_len2 = DotProduct(vertex.normal, vertex.normal);
if (normal_len2 < .9f) {
ERR("model=%s surf=%d vert=%d surf_normal=(%f, %f, %f) vertex.normal=(%f,%f,%f) INVALID len2=%f",
args.mod->name, surface_index, k,
surf_normal[0], surf_normal[1], surf_normal[2],
vertex.normal[0], vertex.normal[1], vertex.normal[2],
normal_len2
);
}
}
VectorCopy(tangent, vertex.tangent);
Vector4Set(vertex.color, 255, 255, 255, 255);
// Store original vertex data for conveyor reasons
if (conv) {
const int vertex_index = conv->vertices_src_offset + k;
ASSERT(vertex_index < args.sizes.conveyors_vertices_count);
args.bmodel->conveyors_vertices[vertex_index] = vertex;
}
//DEBUG(" p[%d]=(%f,%f,%f)", k, vertex.pos[0], vertex.pos[1], vertex.pos[2]);
*(p_vert++) = vertex;
// Write vertex window: p[0] = first, p[1] = prev, p[2] = current
VectorCopy(in_vertex->position, p[Q_min(k, 2)]);
// Ray tracing apparently expects triangle list only (although spec is not very clear about this kekw)
if (k > 1) {
// Check for collinear points/degenerate triangles
vec3_t tri_normal;
computeNormal(p[0], p[1], p[2], tri_normal);
const float area2 = VectorLength2(tri_normal);
if (area2 <= 0.) {
// Do not produce triangle if it has zero area
// NOTE: this is suboptimal in the sense that points that might be necessary for proper
// normal smoothing might be skippedk. In case that this causes undesirable rendering
// artifacts, a more proper triangulation algorithm, that doesn't skip points, would
// be needed. E.g. ear clipping.
/* diagnostics
WARN("surface=%d numedges=%d triangle=%d has degenerate normal, area2=%f",
surface_index, surf->numedges, index_count / 3, area2);
DEBUG(" p[0]=(%f,%f,%f)", p[0][0], p[0][1], p[0][2]);
DEBUG(" p[%d]=(%f,%f,%f)", k - 1, p[1][0], p[1][1], p[1][2]);
DEBUG(" p[%d]=(%f,%f,%f)", k, p[2][0], p[2][1], p[2][2]);
*/
} else {
*(p_ind++) = (uint16_t)(vertex_offset + 0);
*(p_ind++) = (uint16_t)(vertex_offset + k - 1);
*(p_ind++) = (uint16_t)(vertex_offset + k);
index_count += 3;
index_offset += 3;
/* diagnostics for degenerate triangles
const float dot = DotProduct(tri_normal, surf_normal) / sqrt(area2);
if (fabs(dot-1.) > 1e-2) {
WARN("surface=%d triangle=%d tri_normal=(%f,%f,%f) sn=(%f,%f,%f) dot=%f",
surface_index, index_count / 3,
tri_normal[0], tri_normal[1], tri_normal[2],
surf_normal[0], surf_normal[1], surf_normal[2],
dot
);
}
*/
} // valid triangle
// Move current vertex to prev
VectorCopy(p[2], p[1]);
} // if (k > 1)
} // for surf->numedges
model_geometry->element_count = index_count;
vertex_offset += surf->numedges;
} // for mod->nummodelsurfaces
}
ASSERT(args.sizes.num_surfaces == num_geometries);
ASSERT(args.sizes.animated_count == animated_count);
ASSERT(args.sizes.conveyors_count == conveyors_count);
ASSERT(args.sizes.conveyors_vertices_count == conveyors_vertices_count);
return true;
}
static qboolean createRenderModel( const model_t *mod, vk_brush_model_t *bmodel, const model_sizes_t sizes, qboolean is_worldmodel ) {
bmodel->geometry = R_GeometryRangeAlloc(sizes.num_vertices, sizes.num_indices);
if (!bmodel->geometry.block_handle.size) {
ERR("Cannot allocate geometry for %s", mod->name );
return false;
}
vk_render_geometry_t *const geometries = Mem_Malloc(vk_core.pool, sizeof(vk_render_geometry_t) * sizes.num_surfaces);
bmodel->surface_to_geometry_index = Mem_Malloc(vk_core.pool, sizeof(int) * mod->nummodelsurfaces);
for (int i = 0; i < mod->nummodelsurfaces; ++i)
bmodel->surface_to_geometry_index[i] = -1;
bmodel->animated_indexes = Mem_Malloc(vk_core.pool, sizeof(int) * sizes.animated_count);
bmodel->animated_indexes_count = sizes.animated_count;
if (sizes.animated_count > MAX_ANIMATED_TEXTURES) {
WARN("Too many animated textures %d for model \"%s\" some surfaces can be static", sizes.animated_count, mod->name);
}
if (sizes.conveyors_count > 0) {
ASSERT(sizes.conveyors_vertices_count > 3);
bmodel->conveyors_count = sizes.conveyors_count;
bmodel->conveyors_vertices = Mem_Malloc(vk_core.pool, sizeof(vk_vertex_t) * sizes.conveyors_vertices_count);
bmodel->conveyors = Mem_Malloc(vk_core.pool, sizeof(r_conveyor_t) * sizes.conveyors_count);
}
const r_geometry_range_lock_t geom_lock = R_GeometryRangeLock(&bmodel->geometry);
const qboolean fill_result = fillBrushSurfaces((fill_geometries_args_t){
.mod = mod,
.bmodel = bmodel,
.sizes = sizes,
.base_vertex_offset = bmodel->geometry.vertices.unit_offset,
.base_index_offset = bmodel->geometry.indices.unit_offset,
.out_geometries = geometries,
.out_vertices = geom_lock.vertices,
.out_indices = geom_lock.indices,
.is_worldmodel = is_worldmodel,
});
R_GeometryRangeUnlock( &geom_lock );
if (!fill_result) {
// TODO unlock and free buffers if failed? Currently we can't free geometry range, as it is being implicitly referenced by staging queue. Flush staging and free?
// This shouldn't really happen btw, kind of unrecoverable for now tbh.
// Also, we might just handle it, as the only reason it can fail is 16 bit index overflow.
// I. Split into smaller geometries sets.
// II. Make indices 32 bit
return false;
}
if (!R_RenderModelCreate(&bmodel->render_model, (vk_render_model_init_t){
.name = mod->name,
.geometries = geometries,
.geometries_count = sizes.num_surfaces,
.dynamic = false,
})) {
ERR("Could not create render model for brush model %s", mod->name);
return false;
}
return true;
}
qboolean R_BrushModelLoad( model_t *mod, qboolean is_worldmodel ) {
if (mod->cache.data) {
WARN("Model %s was already loaded", mod->name );
return true;
}
DEBUG("%s: %s flags=%08x", __FUNCTION__, mod->name, mod->flags);
vk_brush_model_t *bmodel = Mem_Calloc(vk_core.pool, sizeof(*bmodel));
ASSERT(g_brush.models_count < COUNTOF(g_brush.models));
g_brush.models[g_brush.models_count++] = bmodel;
bmodel->engine_model = mod;
bmodel->patch_rendermode = -1;
mod->cache.data = bmodel;
Matrix4x4_LoadIdentity(bmodel->prev_transform);
bmodel->prev_time = gpGlobals->time;
const model_sizes_t sizes = computeSizes( mod, is_worldmodel );
if (is_worldmodel) {
tglob.current_map_has_surf_sky = sizes.sky_surfaces_count != 0;
DEBUG("sky_surfaces_count=%d, current_map_has_surf_sky=%d", sizes.sky_surfaces_count, tglob.current_map_has_surf_sky);
}
if (sizes.num_surfaces != 0) {
if (!createRenderModel(mod, bmodel, sizes, is_worldmodel)) {
ERR("Could not load brush model %s", mod->name);
// FIXME Cannot deallocate bmodel as we might still have staging references to its memory
return false;
}
}
if (sizes.water.surfaces) {
if (!brushCreateWaterModel(mod, &bmodel->water, sizes.water, BrushSurface_Water, is_worldmodel)) {
ERR("Could not load brush water model %s", mod->name);
// FIXME Cannot deallocate bmodel as we might still have staging references to its memory
return false;
}
}
if (sizes.side_water.surfaces) {
if (!brushCreateWaterModel(mod, &bmodel->water_sides, sizes.side_water, BrushSurface_WaterSide, is_worldmodel)) {
ERR("Could not load brush water_side model %s", mod->name);
// FIXME Cannot deallocate bmodel as we might still have staging references to its memory
return false;
}
}
g_brush.stat.total_vertices += sizes.num_indices + sizes.water.vertices + sizes.side_water.vertices;
g_brush.stat.total_indices += sizes.num_vertices + sizes.water.indices + sizes.side_water.indices;
DEBUG("Model %s loaded surfaces: %d (of %d); total vertices: %u, total indices: %u",
mod->name, bmodel->render_model.num_geometries, mod->nummodelsurfaces, g_brush.stat.total_vertices, g_brush.stat.total_indices);
return true;
}
static void R_BrushModelDestroy( vk_brush_model_t *bmodel ) {
ASSERT(bmodel->engine_model);
DEBUG("%s: %s", __FUNCTION__, bmodel->engine_model->name);
ASSERT(bmodel->engine_model->cache.data == bmodel);
ASSERT(bmodel->engine_model->type == mod_brush);
if (bmodel->dynamic_polylights)
Mem_Free(bmodel->dynamic_polylights);
if (bmodel->conveyors_vertices)
Mem_Free(bmodel->conveyors_vertices);
if (bmodel->conveyors)
Mem_Free(bmodel->conveyors);
if (bmodel->water.surfaces_count) {
R_RenderModelDestroy(&bmodel->water.render_model);
Mem_Free((int*)bmodel->water.surfaces_indices);
Mem_Free(bmodel->water.render_model.geometries);
R_GeometryRangeFree(&bmodel->water.geometry);
}
if (bmodel->water_sides.surfaces_count) {
R_RenderModelDestroy(&bmodel->water_sides.render_model);
Mem_Free((int*)bmodel->water_sides.surfaces_indices);
Mem_Free(bmodel->water_sides.render_model.geometries);
R_GeometryRangeFree(&bmodel->water_sides.geometry);
}
R_RenderModelDestroy(&bmodel->render_model);
if (bmodel->animated_indexes)
Mem_Free(bmodel->animated_indexes);
if (bmodel->surface_to_geometry_index)
Mem_Free(bmodel->surface_to_geometry_index);
if (bmodel->render_model.geometries) {
Mem_Free(bmodel->render_model.geometries);
R_GeometryRangeFree(&bmodel->geometry);
}
bmodel->engine_model->cache.data = NULL;
Mem_Free(bmodel);
}
void R_BrushModelDestroyAll( void ) {
DEBUG("Destroying %d brush models", g_brush.models_count);
for( int i = 0; i < g_brush.models_count; i++ )
R_BrushModelDestroy(g_brush.models[i]);
g_brush.stat.total_vertices = 0;
g_brush.stat.total_indices = 0;
g_brush.models_count = 0;
memset(g_brush.conn.edges, 0, sizeof(*g_brush.conn.edges) * g_brush.conn.edges_capacity);
memset(g_brush.conn.vertices, 0, sizeof(*g_brush.conn.vertices) * g_brush.conn.vertices_capacity);
}
static float computeArea(vec3_t *vertices, int vertices_count) {
vec3_t normal = {0, 0, 0};
for (int i = 2; i < vertices_count; ++i) {
vec3_t e[2], lnormal;
VectorSubtract(vertices[i-0], vertices[0], e[0]);
VectorSubtract(vertices[i-1], vertices[0], e[1]);
CrossProduct(e[0], e[1], lnormal);
VectorAdd(lnormal, normal, normal);
}
return VectorLength(normal);
}
static qboolean loadPolyLight(rt_light_add_polygon_t *out_polygon, const model_t *mod, const int surface_index, const msurface_t *surf, const vec3_t emissive) {
(*out_polygon) = (rt_light_add_polygon_t){0};
out_polygon->num_vertices = Q_min(7, surf->numedges);
// TODO split, don't clip
if (surf->numedges > 7)
WARN_THROTTLED(10, "emissive surface %d has %d vertices; clipping to 7", surface_index, surf->numedges);
VectorCopy(emissive, out_polygon->emissive);
for (int i = 0; i < out_polygon->num_vertices; ++i) {
const int iedge = mod->surfedges[surf->firstedge + i];
const medge_t *edge = mod->edges + (iedge >= 0 ? iedge : -iedge);
const mvertex_t *vertex = mod->vertexes + (iedge >= 0 ? edge->v[0] : edge->v[1]);
VectorCopy(vertex->position, out_polygon->vertices[i]);
}
const float area = computeArea(out_polygon->vertices, out_polygon->num_vertices);
if (area <= 0) {
ERR("%s: emissive surface=%d has area=%f, skipping", __FUNCTION__, surface_index, area);
return false;
}
out_polygon->surface = surf;
return true;
}
void R_VkBrushModelCollectEmissiveSurfaces( const struct model_s *mod, qboolean is_worldmodel ) {
vk_brush_model_t *const bmodel = mod->cache.data;
ASSERT(bmodel);
const xvk_mapent_func_any_t *func_any = getModelFuncAnyPatch(mod);
const qboolean is_static = is_worldmodel || (func_any && func_any->origin_patched);
typedef struct {
int model_surface_index;
int surface_index;
const msurface_t *surf;
vec3_t emissive;
qboolean is_water;
} emissive_surface_t;
emissive_surface_t emissive_surfaces[MAX_SURFACE_LIGHTS];
int geom_indices[MAX_SURFACE_LIGHTS];
int emissive_surfaces_count = 0;
// Load list of all emissive surfaces
for( int i = 0; i < mod->nummodelsurfaces; ++i) {
const int surface_index = mod->firstmodelsurface + i;
const msurface_t *surf = mod->surfaces + surface_index;
const brush_surface_type_e type = getSurfaceType(surf, surface_index, is_worldmodel);
switch (type) {
case BrushSurface_Regular:
case BrushSurface_Water:
// No known cases, also needs to be dynamic case BrushSurface_WaterSide:
break;
// Animated textures are enumerated in `R_BrushModelDraw()` and are added as dynamic lights
// when their current frame is emissive. Do not add such surfaces here to avoid adding them twice.
// TODO: Most of the animated surfaces are techically static: i.e. they don't really move.
// Make a special case for static lights that can be off.
case BrushSurface_Animated:
default:
continue;
}
const int tex_id = surf->texinfo->texture->gl_texturenum; // TODO animation?
vec3_t emissive;
const xvk_patch_surface_t *const psurf = R_VkPatchGetSurface(surface_index);
if (psurf && (psurf->flags & Patch_Surface_Emissive)) {
VectorCopy(psurf->emissive, emissive);
} else if (RT_GetEmissiveForTexture(emissive, tex_id)) {
// emissive
} else {
// not emissive, continue to the next
continue;
}
DEBUG("%d: i=%d surf_index=%d tex_id=%d patch=%d(%#x) => emissive=(%f,%f,%f)", emissive_surfaces_count, i, surface_index, tex_id, !!psurf, psurf?psurf->flags:0, emissive[0], emissive[1], emissive[2]);
if (emissive_surfaces_count == MAX_SURFACE_LIGHTS) {
ERR("Too many emissive surfaces for model %s: max=%d", mod->name, MAX_SURFACE_LIGHTS);
break;
}
emissive_surface_t* const surface = &emissive_surfaces[emissive_surfaces_count++];
surface->model_surface_index = i;
surface->surface_index = surface_index;
surface->surf = surf;
surface->is_water = type == BrushSurface_Water;
VectorCopy(emissive, surface->emissive);
}
// Clear old per-geometry emissive values. The new emissive values will be assigned by the loop below only to the relevant geoms
// This is relevant for updating lights during development
for (int i = 0; i < bmodel->render_model.num_geometries; ++i) {
vk_render_geometry_t *const geom = bmodel->render_model.geometries + i;
VectorClear(geom->emissive);
}
// Non-static brush models may move around and so must have their emissive surfaces treated as dynamic
if (!is_static) {
if (bmodel->dynamic_polylights)
Mem_Free(bmodel->dynamic_polylights);
bmodel->dynamic_polylights_count = 0;
bmodel->dynamic_polylights = Mem_Malloc(vk_core.pool, sizeof(bmodel->dynamic_polylights[0]) * emissive_surfaces_count);
}
// Apply all emissive surfaces found
int geom_indices_count = 0;
for (int i = 0; i < emissive_surfaces_count; ++i) {
const emissive_surface_t* const s = emissive_surfaces + i;
rt_light_add_polygon_t polylight;
if (!loadPolyLight(&polylight, mod, s->surface_index, s->surf, s->emissive))
continue;
// func_any surfaces do not really belong to BSP+PVS system, so they can't be used
// for lights visibility calculation directly.
if (func_any && func_any->origin_patched) {
// TODO this is not really dynamic, but this flag signals using MovingSurface visibility calc
polylight.dynamic = true;
matrix3x4 m;
Matrix3x4_LoadIdentity(m);
Matrix3x4_SetOrigin(m, func_any->origin[0], func_any->origin[1], func_any->origin[2]);
polylight.transform_row = &m;
}
// Static emissive surfaces are added immediately, as they are drawn all the time.
// Non-static ones will be applied later when the model is actually rendered
if (is_static) {
RT_LightAddPolygon(&polylight);
/* TODO figure out when this is needed.
* This is needed in cases where we can dive into emissive acid, which should illuminate what's under it
* Likely, this is not a correct fix, though, see https://github.com/w23/xash3d-fwgs/issues/56
if (s->is_water) {
// Add backside for water
for (int i = 0; i < polylight.num_vertices; ++i) {
vec3_t tmp;
VectorCopy(polylight.vertices[i], tmp);
VectorCopy(polylight.vertices[polylight.num_vertices-1-i], polylight.vertices[i]);
VectorCopy(tmp, polylight.vertices[polylight.num_vertices-1-i]);
RT_LightAddPolygon(&polylight);
}
}
*/
} else {
ASSERT(bmodel->dynamic_polylights_count < emissive_surfaces_count);
bmodel->dynamic_polylights[bmodel->dynamic_polylights_count++] = polylight;
}
// Assign the emissive value to the right geometry
if (bmodel->surface_to_geometry_index) { // Can be absent for water-only models
const int geom_index = bmodel->surface_to_geometry_index[s->model_surface_index];
if (geom_index != -1) { // can be missing for water surfaces
ASSERT(geom_index >= 0);
ASSERT(geom_index < bmodel->render_model.num_geometries);
ASSERT(geom_indices_count < COUNTOF(geom_indices));
geom_indices[geom_indices_count++] = geom_index;
VectorCopy(polylight.emissive, bmodel->render_model.geometries[geom_index].emissive);
}
}
}
if (emissive_surfaces_count > 0) {
// Update emissive values in kusochki. This is required because initial R_BrushModelLoad happens before we've read
// RAD data in vk_light.c, so the emissive values are empty. This is the place and time where we actually get to
// know them, so let's fixup things.
// TODO minor optimization: sort geom_indices to have a better chance for them to be sequential
{
// Make sure that staging has been flushed.
// Updating materials leads to staging an upload to the same memory that we've just staged an upload to.
// This doesn't please the validator.
// Ensure that these uploads are not mixed into the same unsynchronized stream.
// TODO this might be not great for performance (extra waiting for GPU), so a better solution should be considered. E.g. tracking and barrier-syncing regions to-be-reuploaded.
R_VkStagingFlushSync();
}
R_RenderModelUpdateMaterials(&bmodel->render_model, geom_indices, geom_indices_count);
INFO("Loaded %d polylights for %s model %s", emissive_surfaces_count, is_static ? "static" : "movable", mod->name);
}
}
void R_BrushUnloadTextures( model_t *mod )
{
int i;
for( i = 0; i < mod->numtextures; i++ )
{
texture_t *tx = mod->textures[i];
if( !tx || tx->gl_texturenum == tglob.defaultTexture )
continue; // free slot
R_TextureFree( tx->gl_texturenum ); // main texture
R_TextureFree( tx->fb_texturenum ); // luma texture
}
}