Files
fs2open.github.com/code/model/modelinterp.cpp
T
BMagnu 1f5525e04f Fix virtual POF crashes (#7327)
* Ensure model data isn't deleted twice

* shared_ptr-ify model data

* fix virtual pof filename issue

* Fix tests

* remove redundant memsets

* Clang Tid
2026-03-31 10:02:24 -04:00

3234 lines
83 KiB
C++

/*
* Copyright (C) Volition, Inc. 1999. All rights reserved.
*
* All source code herein is the property of Volition, Inc. You may not sell
* or otherwise commercially exploit the source or things you created based on the
* source.
*
*/
#define MODEL_LIB
#include "bmpman/bmpman.h"
#include "cmdline/cmdline.h"
#include "debugconsole/console.h"
#include "gamesequence/gamesequence.h"
#include "gamesnd/gamesnd.h"
#include "globalincs/alphacolors.h"
#include "globalincs/linklist.h"
#include "graphics/2d.h"
#include "graphics/util/GPUMemoryHeap.h"
#include "io/key.h"
#include "io/timer.h"
#include "math/fvi.h"
#include "math/staticrand.h"
#include "mission/missionparse.h"
#include "model/modelrender.h"
#include "model/modelsinc.h"
#include "nebula/neb.h"
#include "parse/parselo.h"
#include "particle/particle.h"
#include "render/3dinternal.h"
#include "ship/ship.h"
#include "ship/shipfx.h"
#include "tracing/Monitor.h"
#include "tracing/tracing.h"
#include "utils/Random.h"
#include "weapon/shockwave.h"
#include <climits>
float model_radius = 0;
// Some debug variables used externally for displaying stats
#ifndef NDEBUG
int modelstats_num_polys = 0;
int modelstats_num_polys_drawn = 0;
int modelstats_num_verts = 0;
int modelstats_num_sortnorms = 0;
int modelstats_num_boxes = 0;
#endif
typedef struct model_light {
ubyte r, g, b;
} model_light;
// a lighting object
typedef struct model_light_object {
model_light *lights;
int objnum;
int skip;
int skip_max;
} model_light_object;
struct bsp_vertex
{
vec3d position;
vec3d normal;
uv_pair tex_coord;
ubyte r, g, b, a;
};
struct bsp_polygon
{
uint Start_index;
uint Num_verts;
int texture;
};
class bsp_polygon_data
{
SCP_vector<vec3d> Vertex_list;
SCP_vector<vec3d> Normal_list;
SCP_vector<bsp_vertex> Polygon_vertices;
SCP_vector<bsp_polygon> Polygons;
ubyte* Lights;
int Num_polies[MAX_MODEL_TEXTURES];
int Num_verts[MAX_MODEL_TEXTURES];
int Num_flat_polies;
int Num_flat_verts;
int bsp_data_size;
void process_bsp(int offset, ubyte* bsp_data);
void process_defpoints(int off, ubyte* bsp_data);
void process_sortnorm(int offset, ubyte* bsp_data);
void process_sortnorm2(int offset, ubyte* bsp_data);
void process_tmap(int offset, ubyte* bsp_data);
void process_tmap2(int offset, ubyte* bsp_data);
void process_flat(int offset, ubyte* bsp_data);
public:
bsp_polygon_data(ubyte* bsp_data, int bsp_data_size);
int get_num_triangles(int texture);
int get_num_lines(int texture);
void generate_triangles(int texture, vertex *vert_ptr, vec3d* norm_ptr);
void generate_lines(int texture, vertex *vert_ptr);
SCP_set<int> get_textures_used() const;
void replace_textures_used(const SCP_map<int, int>& replacementMap);
};
/**
* @brief Vertex structure for passing data to the GPU
*/
struct interp_vertex {
uv_pair uv;
vec3d normal;
vec4 tangent;
float modelId;
vec3d pos;
};
// -----------------------
// Local variables
//
static uint Num_interp_verts_allocated = 0;
vec3d **Interp_verts = NULL;
static vertex *Interp_points = NULL;
static uint Interp_num_verts = 0;
static float Interp_box_scale = 1.0f; // this is used to scale both detail boxes and spheres
// -------------------------------------------------------------------
// lighting save stuff
//
model_light_object Interp_lighting_temp;
model_light_object *Interp_lighting = &Interp_lighting_temp;
int Interp_use_saved_lighting = 0;
int Interp_saved_lighting_full = 0;
//
// lighting save stuff
// -------------------------------------------------------------------
static uint Num_interp_norms_allocated = 0;
static vec3d **Interp_norms = NULL;
static ubyte *Interp_light_applied = NULL;
static uint Interp_num_norms = 0;
static ubyte *Interp_lights;
// Stuff to control rendering parameters
static color Interp_outline_color;
static int Interp_detail_level_locked = -1;
static uint Interp_flags = 0;
// If non-zero, then the subobject gets scaled by Interp_thrust_scale.
int Interp_thrust_scale_subobj = 0;
float Interp_thrust_scale = 0.1f;
static float Interp_thrust_scale_x = 0.0f;//added -bobboau
static float Interp_thrust_scale_y = 0.0f;//added -bobboau
static int Interp_thrust_bitmap = -1;
static int Interp_thrust_glow_bitmap = -1;
static float Interp_thrust_glow_noise = 1.0f;
static bool Interp_afterburner = false;
// Bobboau's thruster stuff
static int Interp_secondary_thrust_glow_bitmap = -1;
static int Interp_tertiary_thrust_glow_bitmap = -1;
static int Interp_distortion_thrust_bitmap = -1;
static float Interp_thrust_glow_rad_factor = 1.0f;
static float Interp_secondary_thrust_glow_rad_factor = 1.0f;
static float Interp_tertiary_thrust_glow_rad_factor = 1.0f;
static float Interp_distortion_thrust_rad_factor = 1.0f;
static float Interp_distortion_thrust_length_factor = 1.0f;
static float Interp_thrust_glow_len_factor = 1.0f;
static vec3d Interp_thrust_rotvel = ZERO_VECTOR;
static bool Interp_draw_distortion = true;
// Bobboau's warp stuff
static float Interp_warp_scale_x = 1.0f;
static float Interp_warp_scale_y = 1.0f;
static float Interp_warp_scale_z = 1.0f;
// if != -1, use this bitmap when rendering ship insignias
static int Interp_insignia_bitmap = -1;
// if != -1, use this bitmap when rendering with a forced texture
static int Interp_forced_bitmap = -1;
// our current level of detail (LOD)
int Interp_detail_level = 0;
// forward references
int model_should_render_engine_glow(int objnum, int bank_obj);
void model_deallocate_interp_data()
{
if (Interp_verts != nullptr) {
vm_free(Interp_verts);
Interp_verts = nullptr;
}
if (Interp_points != nullptr) {
vm_free(Interp_points);
Interp_points = nullptr;
}
if (Interp_norms != nullptr) {
vm_free(Interp_norms);
Interp_norms = nullptr;
}
if (Interp_light_applied != nullptr) {
vm_free(Interp_light_applied);
Interp_light_applied = nullptr;
}
if (Interp_lighting_temp.lights != nullptr) {
vm_free(Interp_lighting_temp.lights);
Interp_lighting_temp.lights = nullptr;
}
Num_interp_verts_allocated = 0;
Num_interp_norms_allocated = 0;
}
extern void model_collide_allocate_point_list(int n_points);
extern void model_collide_free_point_list();
void model_allocate_interp_data(uint n_verts, uint n_norms)
{
static ubyte dealloc = 0;
if (!dealloc) {
atexit(model_deallocate_interp_data);
atexit(model_collide_free_point_list);
dealloc = 1;
}
Assert( (n_verts || Num_interp_verts_allocated) && (n_norms || Num_interp_norms_allocated) );
if (n_verts > Num_interp_verts_allocated) {
if (Interp_verts != NULL) {
vm_free(Interp_verts);
Interp_verts = NULL;
}
// Interp_verts can't be reliably realloc'd so free and malloc it on each resize (no data needs to be carried over)
Interp_verts = (vec3d**) vm_malloc( n_verts * sizeof(vec3d *) );
Interp_points = (vertex*) vm_realloc( Interp_points, n_verts * sizeof(vertex) );
Num_interp_verts_allocated = n_verts;
// model collide needs a similar size to resize it based on this new value
model_collide_allocate_point_list( n_verts );
}
if (n_norms > Num_interp_norms_allocated) {
if (Interp_norms != NULL) {
vm_free(Interp_norms);
Interp_norms = NULL;
}
// Interp_norms can't be reliably realloc'd so free and malloc it on each resize (no data needs to be carried over)
Interp_norms = (vec3d**) vm_malloc( n_norms * sizeof(vec3d *) );
// these next two lighting things aren't values that need to be carried over, but we need to make sure they are 0 by default
if (Interp_light_applied != NULL) {
vm_free(Interp_light_applied);
Interp_light_applied = NULL;
}
if (Interp_lighting_temp.lights != NULL) {
vm_free(Interp_lighting_temp.lights);
Interp_lighting_temp.lights = NULL;
}
Interp_light_applied = (ubyte*) vm_malloc( n_norms * sizeof(ubyte) );
Interp_lighting_temp.lights = (model_light*) vm_malloc( n_norms * sizeof(model_light) );
memset( Interp_light_applied, 0, n_norms * sizeof(ubyte) );
memset( Interp_lighting_temp.lights, 0, n_norms * sizeof(model_light) );
Num_interp_norms_allocated = n_norms;
}
Interp_num_verts = n_verts;
Interp_num_norms = n_norms;
// check that everything is still usable (works in release and debug builds)
Verify( Interp_points != NULL );
Verify( Interp_verts != NULL );
Verify( Interp_norms != NULL );
Verify( Interp_light_applied != NULL );
}
void interp_clear_instance()
{
Interp_thrust_scale = 0.1f;
Interp_thrust_scale_x = 0.0f;//added-Bobboau
Interp_thrust_scale_y = 0.0f;//added-Bobboau
Interp_thrust_bitmap = -1;
Interp_thrust_glow_bitmap = -1;
Interp_thrust_glow_noise = 1.0f;
Interp_insignia_bitmap = -1;
Interp_afterburner = false;
// Bobboau's thruster stuff
{
Interp_thrust_glow_rad_factor = 1.0f;
Interp_secondary_thrust_glow_bitmap = -1;
Interp_secondary_thrust_glow_rad_factor = 1.0f;
Interp_tertiary_thrust_glow_bitmap = -1;
Interp_tertiary_thrust_glow_rad_factor = 1.0f;
Interp_thrust_glow_len_factor = 1.0f;
vm_vec_zero(&Interp_thrust_rotvel);
}
Interp_box_scale = 1.0f;
Interp_detail_level_locked = -1;
Interp_forced_bitmap = -1;
}
/**
* Scales the engines thrusters by this much
*/
void model_set_thrust(int /*model_num*/, mst_info *mst)
{
if (mst == NULL) {
Int3();
return;
}
Interp_thrust_scale = mst->length.xyz.z;
Interp_thrust_scale_x = mst->length.xyz.x;
Interp_thrust_scale_y = mst->length.xyz.y;
CLAMP(Interp_thrust_scale, 0.1f, 1.0f);
Interp_thrust_bitmap = mst->primary_bitmap;
Interp_thrust_glow_bitmap = mst->primary_glow_bitmap;
Interp_secondary_thrust_glow_bitmap = mst->secondary_glow_bitmap;
Interp_tertiary_thrust_glow_bitmap = mst->tertiary_glow_bitmap;
Interp_distortion_thrust_bitmap = mst->distortion_bitmap;
Interp_thrust_glow_noise = mst->glow_noise;
Interp_afterburner = mst->use_ab;
Interp_thrust_rotvel = mst->rotvel;
Interp_thrust_glow_rad_factor = mst->glow_rad_factor;
Interp_secondary_thrust_glow_rad_factor = mst->secondary_glow_rad_factor;
Interp_tertiary_thrust_glow_rad_factor = mst->tertiary_glow_rad_factor;
Interp_thrust_glow_len_factor = mst->glow_length_factor;
Interp_distortion_thrust_rad_factor = mst->distortion_rad_factor;
Interp_distortion_thrust_length_factor = mst->distortion_length_factor;
Interp_draw_distortion = mst->draw_distortion;
}
float GEOMETRY_NOISE = 0.0f;
// Point list
// +0 int id
// +4 int size
// +8 int n_verts
// +12 int n_norms
// +16 int offset from start of chunk to vertex data
// +20 n_verts*char norm_counts
// +offset vertex data. Each vertex n is a point followed by norm_counts[n] normals.
void model_interp_defpoints(ubyte * p, polymodel *pm, bsp_info *sm)
{
SCP_UNUSED(pm);
SCP_UNUSED(sm);
uint i, n;
uint nverts = uw(p+8);
uint offset = uw(p+16);
uint next_norm = 0;
uint nnorms = 0;
ubyte * normcount = p+20;
vertex *dest = NULL;
vec3d *src = vp(p+offset);
// Get pointer to lights
Interp_lights = p+20+nverts;
for (i = 0; i < nverts; i++) {
nnorms += normcount[i];
}
// allocate new Interp data if size is greater than we already have ready to use
model_allocate_interp_data(nverts, nnorms);
dest = Interp_points;
Assert( dest != NULL );
#ifndef NDEBUG
modelstats_num_verts += nverts;
#endif
if (Interp_thrust_scale_subobj) {
// Only scale vertices that aren't on the "base" of
// the effect. Base is something Adam decided to be
// anything under 1.5 meters, hence the 1.5f.
float min_thruster_dist = -1.5f;
if ( Interp_flags & MR_IS_MISSILE ) {
min_thruster_dist = 0.5f;
}
for (n=0; n<nverts; n++ ) {
vec3d tmp;
Interp_verts[n] = src;
// Only scale vertices that aren't on the "base" of
// the effect. Base is something Adam decided to be
// anything under 1.5 meters, hence the 1.5f.
if ( src->xyz.z < min_thruster_dist ) {
tmp.xyz.x = src->xyz.x * 1.0f;
tmp.xyz.y = src->xyz.y * 1.0f;
tmp.xyz.z = src->xyz.z * Interp_thrust_scale;
} else {
tmp = *src;
}
g3_rotate_vertex(dest,&tmp);
src++; // move to normal
for (i=0; i<normcount[n]; i++ ) {
Interp_light_applied[next_norm] = 0;
Interp_norms[next_norm] = src;
next_norm++;
src++;
}
dest++;
}
} else if ( (Interp_warp_scale_x != 1.0f) || (Interp_warp_scale_y != 1.0f) || (Interp_warp_scale_z != 1.0f)) {
for (n=0; n<nverts; n++ ) {
vec3d tmp;
Interp_verts[n] = src;
tmp.xyz.x = (src->xyz.x) * Interp_warp_scale_x;
tmp.xyz.y = (src->xyz.y) * Interp_warp_scale_y;
tmp.xyz.z = (src->xyz.z) * Interp_warp_scale_z;
g3_rotate_vertex(dest,&tmp);
src++; // move to normal
for (i=0; i<normcount[n]; i++ ) {
Interp_light_applied[next_norm] = 0;
Interp_norms[next_norm] = src;
next_norm++;
src++;
}
dest++;
}
} else {
vec3d point;
for (n=0; n<nverts; n++ ) {
if(GEOMETRY_NOISE!=0.0f){
GEOMETRY_NOISE = model_radius / 50;
Interp_verts[n] = src;
point.xyz.x = src->xyz.x + frand_range(GEOMETRY_NOISE,-GEOMETRY_NOISE);
point.xyz.y = src->xyz.y + frand_range(GEOMETRY_NOISE,-GEOMETRY_NOISE);
point.xyz.z = src->xyz.z + frand_range(GEOMETRY_NOISE,-GEOMETRY_NOISE);
g3_rotate_vertex(dest, &point);
}else{
Interp_verts[n] = src;
g3_rotate_vertex(dest, src);
}
src++; // move to normal
for (i=0; i<normcount[n]; i++ ) {
Interp_light_applied[next_norm] = 0;
Interp_norms[next_norm] = src;
next_norm++;
src++;
}
dest++;
}
}
Interp_num_norms = next_norm;
}
void model_interp_edge_alpha( ubyte *param_r, ubyte *param_g, ubyte *param_b, vec3d *pnt, vec3d *norm, float alpha, bool invert = false)
{
vec3d r;
vm_vec_sub(&r, &View_position, pnt);
vm_vec_normalize(&r);
float d = vm_vec_dot(&r, norm);
if (d < 0.0f)
d = -d;
if (invert)
*param_r = *param_g = *param_b = ubyte( fl2i((1.0f - d) * 254.0f * alpha));
else
*param_r = *param_g = *param_b = ubyte( fl2i(d * 254.0f * alpha) );
}
int Interp_subspace = 0;
float Interp_subspace_offset_u = 0.0f;
float Interp_subspace_offset_v = 0.0f;
ubyte Interp_subspace_r = 255;
ubyte Interp_subspace_g = 255;
ubyte Interp_subspace_b = 255;
void model_draw_debug_points(const polymodel *pm, const bsp_info *submodel, uint64_t flags )
{
if ( flags & MR_SHOW_OUTLINE_PRESET ) {
return;
}
// Draw a red pivot point
gr_set_color(128,0,0);
g3_draw_sphere_ez(&vmd_zero_vector, 2.0f );
// Draw a green center of mass when drawing the hull
if ( submodel && (submodel->parent==-1) ) {
gr_set_color(0,128,0);
g3_draw_sphere_ez( &pm->center_of_mass, 1.0f );
}
if ( submodel ) {
// Draw a blue center point
gr_set_color(0,0,128);
g3_draw_sphere_ez( &submodel->geometric_center, 0.9f );
}
// Draw the bounding box
int i;
vertex pts[8];
if ( submodel ) {
for (i=0; i<8; i++ ) {
g3_rotate_vertex( &pts[i], &submodel->bounding_box[i] );
}
gr_set_color(128,128,128);
g3_draw_line( &pts[0], &pts[1] );
g3_draw_line( &pts[1], &pts[2] );
g3_draw_line( &pts[2], &pts[3] );
g3_draw_line( &pts[3], &pts[0] );
g3_draw_line( &pts[4], &pts[5] );
g3_draw_line( &pts[5], &pts[6] );
g3_draw_line( &pts[6], &pts[7] );
g3_draw_line( &pts[7], &pts[4] );
g3_draw_line( &pts[0], &pts[4] );
g3_draw_line( &pts[1], &pts[5] );
g3_draw_line( &pts[2], &pts[6] );
g3_draw_line( &pts[3], &pts[7] );
} else {
gr_set_color(0,255,0);
vec3d bounding_box[8]; // caclulated fron min/max
model_calc_bound_box(bounding_box, &pm->mins, &pm->maxs);
for (i=0; i<8; i++ ) {
g3_rotate_vertex( &pts[i], &bounding_box[i] );
}
gr_set_color(128,0,0);
g3_draw_line( &pts[0], &pts[1] );
g3_draw_line( &pts[1], &pts[2] );
g3_draw_line( &pts[2], &pts[3] );
g3_draw_line( &pts[3], &pts[0] );
g3_draw_line( &pts[4], &pts[5] );
g3_draw_line( &pts[5], &pts[6] );
g3_draw_line( &pts[6], &pts[7] );
g3_draw_line( &pts[7], &pts[4] );
g3_draw_line( &pts[0], &pts[4] );
g3_draw_line( &pts[1], &pts[5] );
g3_draw_line( &pts[2], &pts[6] );
g3_draw_line( &pts[3], &pts[7] );
}
}
/**
* Debug code to show all the paths of a model
*/
void model_draw_paths_htl( int model_num, uint64_t flags )
{
int i,j;
vec3d pnt;
vec3d prev_pnt;
polymodel * pm;
if ( flags & MR_SHOW_OUTLINE_PRESET ) {
return;
}
pm = model_get(model_num);
if (pm->n_paths<1){
return;
}
int cull = gr_set_cull(0);
for (i=0; i<pm->n_paths; i++ ) {
for (j=0; j<pm->paths[i].nverts; j++ )
{
// Rotate point into world coordinates
pnt = pm->paths[i].verts[j].pos;
// Pnt is now the x,y,z world coordinates of this vert.
// For this example, I am just drawing a sphere at that
// point.
vertex tmp;
g3_rotate_vertex(&tmp,&pnt);
if ( pm->paths[i].verts[j].nturrets > 0 ){
gr_set_color( 0, 0, 255 ); // draw points covered by turrets in blue
} else {
gr_set_color( 255, 0, 0 );
}
g3_render_sphere(&pnt, 0.5f);
if (j)
{
//g3_draw_htl_line(&prev_pnt, &pnt);
g3_render_line_3d(true, &prev_pnt, &pnt);
}
prev_pnt = pnt;
}
}
gr_set_cull(cull);
}
/**
* Docking bay and fighter bay paths
*/
void model_draw_bay_paths_htl(int model_num)
{
int idx, s_idx;
vec3d v1, v2;
polymodel *pm = model_get(model_num);
if(pm == NULL){
return;
}
int cull = gr_set_cull(0);
// render docking bay normals
gr_set_color(0, 255, 0);
for(idx=0; idx<pm->n_docks; idx++){
for(s_idx=0; s_idx<pm->docking_bays[idx].num_slots; s_idx++){
v1 = pm->docking_bays[idx].pnt[s_idx];
vm_vec_scale_add(&v2, &v1, &pm->docking_bays[idx].norm[s_idx], 10.0f);
// draw the point and normal
g3_render_sphere(&v1, 2.0);
//g3_draw_htl_line(&v1, &v2);
g3_render_line_3d(true, &v1, &v2);
}
}
// render figher bay paths
gr_set_color(0, 255, 255);
// iterate through the paths that exist in the polymodel, searching for $bayN pathnames
for (idx = 0; idx<pm->n_paths; idx++) {
if ( !strnicmp(pm->paths[idx].name, NOX("$bay"), 4) ) {
for(s_idx=0; s_idx<pm->paths[idx].nverts-1; s_idx++){
v1 = pm->paths[idx].verts[s_idx].pos;
v2 = pm->paths[idx].verts[s_idx+1].pos;
//g3_draw_htl_line(&v1, &v2);
g3_render_line_3d(true, &v1, &v2);
}
}
}
gr_set_cull(cull);
}
void interp_generate_arc_segment(SCP_vector<vec3d> &arc_segment_points, const vec3d *v1, const vec3d *v2, ubyte depth_limit, ubyte depth)
{
float d = vm_vec_dist_quick( v1, v2 );
constexpr float scaler = 0.30f;
if ( (d < scaler) || (depth > depth_limit) ) {
arc_segment_points.push_back(*v2);
} else {
// divide in half
vec3d tmp;
vm_vec_avg( &tmp, v1, v2 );
tmp.xyz.x += (frand() - 0.5f) * d * scaler;
tmp.xyz.y += (frand() - 0.5f) * d * scaler;
tmp.xyz.z += (frand() - 0.5f) * d * scaler;
// add additional point
interp_generate_arc_segment( arc_segment_points, v1, &tmp, depth_limit, depth+1 );
interp_generate_arc_segment( arc_segment_points, &tmp, v2, depth_limit, depth+1 );
}
}
int Interp_lightning = 1;
DCF_BOOL( Arcs, Interp_lightning )
// Returns one of the following
#define IBOX_ALL_OFF 0
#define IBOX_ALL_ON 1
#define IBOX_SOME_ON_SOME_OFF 2
int interp_box_offscreen( vec3d *min, vec3d *max )
{
if ( key_is_pressed(KEY_LSHIFT) ) {
return IBOX_ALL_ON;
}
vec3d v[8];
v[0].xyz.x = min->xyz.x; v[0].xyz.y = min->xyz.y; v[0].xyz.z = min->xyz.z;
v[1].xyz.x = max->xyz.x; v[1].xyz.y = min->xyz.y; v[1].xyz.z = min->xyz.z;
v[2].xyz.x = max->xyz.x; v[2].xyz.y = max->xyz.y; v[2].xyz.z = min->xyz.z;
v[3].xyz.x = min->xyz.x; v[3].xyz.y = max->xyz.y; v[3].xyz.z = min->xyz.z;
v[4].xyz.x = min->xyz.x; v[4].xyz.y = min->xyz.y; v[4].xyz.z = max->xyz.z;
v[5].xyz.x = max->xyz.x; v[5].xyz.y = min->xyz.y; v[5].xyz.z = max->xyz.z;
v[6].xyz.x = max->xyz.x; v[6].xyz.y = max->xyz.y; v[6].xyz.z = max->xyz.z;
v[7].xyz.x = min->xyz.x; v[7].xyz.y = max->xyz.y; v[7].xyz.z = max->xyz.z;
ubyte and_codes = 0xff;
ubyte or_codes = 0xff;
int i;
for (i=0; i<8; i++ ) {
vertex tmp;
ubyte codes=g3_rotate_vertex( &tmp, &v[i] );
or_codes |= codes;
and_codes &= codes;
}
// If and_codes is set this means that all points lie off to the
// same side of the screen.
if (and_codes) {
return IBOX_ALL_OFF; //all points off screen
}
// If this is set it means at least one of the points is offscreen,
// but they aren't all off to the same side.
if (or_codes) {
return IBOX_SOME_ON_SOME_OFF;
}
// They are all onscreen.
return IBOX_ALL_ON;
}
void model_render_shields( polymodel * pm, uint64_t flags )
{
int i, j;
shield_tri *tri;
vertex pnt0, prev_pnt, tmp = vertex();
if ( flags & MR_SHOW_OUTLINE_PRESET ) {
return;
}
gr_set_color(0, 0, 200 );
// Scan all the triangles in the mesh.
for (i=0; i<pm->shield.ntris; i++ ) {
tri = &pm->shield.tris[i];
if (g3_check_normal_facing(&pm->shield.verts[tri->verts[0]].pos,&tri->norm ) ) {
// Process the vertices.
// Note this rotates each vertex each time it's needed, very dumb.
for (j=0; j<3; j++ ) {
g3_rotate_vertex(&tmp, &pm->shield.verts[tri->verts[j]].pos );
if (j)
g3_draw_line(&prev_pnt, &tmp);
else
pnt0 = tmp;
prev_pnt = tmp;
}
g3_draw_line(&pnt0, &prev_pnt);
}
}
}
int Model_texturing = 1;
int Model_polys = 1;
DCF_BOOL( model_texturing, Model_texturing )
DCF_BOOL( model_polys, Model_polys )
MONITOR( NumModelsRend )
MONITOR( NumHiModelsRend )
MONITOR( NumMedModelsRend )
MONITOR( NumLowModelsRend )
/**
* Draws a bitmap with the specified 3d width & height
* @return 1 if off screen, 0 if not
*/
int model_get_rotated_bitmap_points(vertex *pnt,float angle, float rad, vertex *v)
{
float sa, ca;
int i;
Assert( G3_count == 1 );
sa = sinf(angle);
ca = cosf(angle);
float width, height;
width = height = rad;
v[0].world.xyz.x = (-width*ca - height*sa)*Matrix_scale.xyz.x + pnt->world.xyz.x;
v[0].world.xyz.y = (-width*sa + height*ca)*Matrix_scale.xyz.y + pnt->world.xyz.y;
v[0].world.xyz.z = pnt->world.xyz.z;
v[0].screen.xyw.w = 0.0f;
v[0].texture_position.u = 0.0f;
v[0].texture_position.v = 0.0f;
v[1].world.xyz.x = (width*ca - height*sa)*Matrix_scale.xyz.x + pnt->world.xyz.x;
v[1].world.xyz.y = (width*sa + height*ca)*Matrix_scale.xyz.y + pnt->world.xyz.y;
v[1].world.xyz.z = pnt->world.xyz.z;
v[1].screen.xyw.w = 0.0f;
v[1].texture_position.u = 1.0f;
v[1].texture_position.v = 0.0f;
v[2].world.xyz.x = (width*ca + height*sa)*Matrix_scale.xyz.x + pnt->world.xyz.x;
v[2].world.xyz.y = (width*sa - height*ca)*Matrix_scale.xyz.y + pnt->world.xyz.y;
v[2].world.xyz.z = pnt->world.xyz.z;
v[2].screen.xyw.w = 0.0f;
v[2].texture_position.u = 1.0f;
v[2].texture_position.v = 1.0f;
v[3].world.xyz.x = (-width*ca + height*sa)*Matrix_scale.xyz.x + pnt->world.xyz.x;
v[3].world.xyz.y = (-width*sa - height*ca)*Matrix_scale.xyz.y + pnt->world.xyz.y;
v[3].world.xyz.z = pnt->world.xyz.z;
v[3].screen.xyw.w = 0.0f;
v[3].texture_position.u = 0.0f;
v[3].texture_position.v = 1.0f;
ubyte codes_and=0xff;
float sw,z;
z = pnt->world.xyz.z - rad / 4.0f;
if ( z < 0.0f ) z = 0.0f;
sw = 1.0f / z;
for (i=0; i<4; i++ ) {
//now code the four points
codes_and &= g3_code_vertex(&v[i]);
v[i].flags = 0; // mark as not yet projected
g3_project_vertex(&v[i]);
v[i].screen.xyw.w = sw;
}
if (codes_and)
return 1; //1 means off screen
return 0;
}
float Interp_depth_scale = 1500.0f;
DCF(model_darkening,"Makes models darker with distance")
{
if (dc_optional_string_either("help", "--help")) {
dc_printf( "Usage: model_darkening <float>\n" );
dc_printf("Sets the distance at which to start blacking out models (namely asteroids).\n");
return;
}
if (dc_optional_string_either("status", "--status") || dc_optional_string_either("?", "--?")) {
dc_printf( "model_darkening = %.1f\n", Interp_depth_scale );
return;
}
dc_stuff_float(&Interp_depth_scale);
dc_printf("model_darkening set to %.1f\n", Interp_depth_scale);
}
// tmp_detail_level
// 0 - Max
// 1
// 2
// 3
// 4 - None
static_assert(MAX_DETAIL_VALUE == 4, "MAX_DETAIL_VALUE is assumed to be 4 in SystemVars.cpp");
/**
* Find the distance-squared from p0 to the closest point on a box. Fills in closest_pt.
* The box's dimensions are from 'min' to 'max'.
*/
float interp_closest_dist_sq_to_box( vec3d *closest_pt, const vec3d *p0, const vec3d *min, const vec3d *max )
{
auto origin = p0->a1d;
auto minB = min->a1d;
auto maxB = max->a1d;
auto coord = closest_pt->a1d;
bool inside = true;
int i;
for (i=0; i<3; i++ ) {
if ( origin[i] < minB[i] ) {
coord[i] = minB[i];
inside = false;
} else if (origin[i] > maxB[i] ) {
coord[i] = maxB[i];
inside = false;
} else {
coord[i] = origin[i];
}
}
if ( inside ) {
return 0.0f;
}
return vm_vec_dist_squared(closest_pt, p0);
}
// Finds the closest point on a model to a point in space. Actually only finds a point
// on the bounding box of the model.
// Given:
// model_num Which model
// orient Orientation of the model
// pos Position of the model
// eye_pos Point that you want to find the closest point to
// Returns:
// distance from eye_pos to closest_point. 0 means eye_pos is
// on or inside the bounding box.
// Also fills in outpnt with the actual closest point (in local coordinates).
float model_find_closest_point( vec3d *outpnt, int model_num, int submodel_num, const matrix *orient, const vec3d *pos, const vec3d *eye_pos )
{
vec3d tempv, eye_rel_pos;
polymodel *pm = model_get(model_num);
if ( submodel_num < 0 ) {
submodel_num = pm->detail[0];
}
// Rotate eye pos into object coordinates
vm_vec_sub(&tempv, pos, eye_pos);
vm_vec_rotate(&eye_rel_pos, &tempv, orient);
return fl_sqrt( interp_closest_dist_sq_to_box( outpnt, &eye_rel_pos, &pm->submodel[submodel_num].min, &pm->submodel[submodel_num].max ) );
}
// Like the above, but finds the closest two points to each other.
float model_find_closest_points(vec3d *outpnt1, int model_num1, int submodel_num1, const matrix *orient1, const vec3d *pos1, vec3d *outpnt2, int model_num2, int submodel_num2, const matrix *orient2, const vec3d *pos2)
{
polymodel *pm1 = model_get(model_num1);
if (submodel_num1 < 0)
submodel_num1 = pm1->detail[0];
polymodel *pm2 = model_get(model_num2);
if (submodel_num2 < 0)
submodel_num2 = pm2->detail[0];
// determine obj2's bounding box
vec3d bounding_box[8];
model_calc_bound_box(bounding_box, &pm2->submodel[submodel_num2].min, &pm2->submodel[submodel_num2].max);
float closest_dist_sq = -1.0f;
// check each point on it
for (const auto &pt : bounding_box)
{
vec3d temp, rel_pt;
// find world coordinates of this point
vm_vec_unrotate(&temp, &pt, orient2);
vm_vec_add(&rel_pt, &temp, pos2);
// now find coordinates relative to obj1
vm_vec_sub(&temp, pos1, &rel_pt);
vm_vec_rotate(&rel_pt, &temp, orient1);
// test this point
float dist_sq = interp_closest_dist_sq_to_box(&temp, &rel_pt, &pm1->submodel[submodel_num1].min, &pm1->submodel[submodel_num1].max);
if (closest_dist_sq < 0.0f || dist_sq < closest_dist_sq)
{
closest_dist_sq = dist_sq;
// Note: As in the other function, both of these points are
// in local coordinates relative to each of their models.
*outpnt1 = temp;
*outpnt2 = pt;
}
}
// we have now found the closest point
return fl_sqrt(closest_dist_sq);
}
int tiling = 1;
DCF(tiling, "Toggles rendering of tiled textures (default is on)")
{
if (dc_optional_string_either("status", "--status") || dc_optional_string_either("?", "--?")) {
dc_printf("Tiled textures are %s", tiling ? "ON" : "OFF");
return;
}
tiling = !tiling;
if(tiling){
dc_printf("Tiled textures\n");
} else {
dc_printf("Non-tiled textures\n");
}
}
void moldel_calc_facing_pts( vec3d *top, vec3d *bot, vec3d *fvec, vec3d *pos, float w, float /*z_add*/, vec3d *Eyeposition )
{
vec3d uvec, rvec;
vec3d temp;
temp = *pos;
vm_vec_sub( &rvec, Eyeposition, &temp );
vm_vec_normalize( &rvec );
vm_vec_cross(&uvec,fvec,&rvec);
vm_vec_normalize(&uvec);
vm_vec_scale_add( top, &temp, &uvec, w/2.0f );
vm_vec_scale_add( bot, &temp, &uvec, -w/2.0f );
}
vec3d submodel_get_random_point(int model_num, int submodel_num, int seed)
{
polymodel *pm = model_get(model_num);
if (pm != NULL) {
if ( submodel_num < 0 ) {
submodel_num = pm->detail[0];
}
bsp_collision_tree *tree = model_get_bsp_collision_tree(pm->submodel[submodel_num].collision_tree_index);
int nv = tree->n_verts;
// this is not only because of the immediate div-0 error but also because of the less immediate expectation for at least one point (preferably two) to be found
if (nv <= 0) {
Error(LOCATION, "Model %d ('%s') must have at least one point in its collision tree!", model_num, (pm == NULL) ? "<null model?!?>" : pm->filename);
// in case people ignore the error...
return vmd_zero_vector;
}
int seed_num = seed == -1 ? Random::next() : seed;
int vn1 = static_rand(seed_num) % nv;
return tree->point_list[vn1];
} else {
Assertion(false, "submodel_get_random_point called on an invalid model!");
return vmd_zero_vector;
}
}
void submodel_get_cross_sectional_avg_pos(int model_num, int submodel_num, float z_slice_pos, vec3d* pos)
{
polymodel* pm = model_get(model_num);
if (pm != nullptr) {
if (submodel_num < 0) {
submodel_num = pm->detail[0];
}
bsp_collision_tree* tree = model_get_bsp_collision_tree(pm->submodel[submodel_num].collision_tree_index);
int nv = tree->n_verts;
// this is not only because of the immediate div-0 error but also because of the less immediate expectation for at least one point (preferably two) to be found
if (nv <= 0) {
Error(LOCATION, "Model %d ('%s') must have at least one point from submodel_get_cross_sectional_avg_pos!", model_num, (pm == nullptr) ? "<null model?!?>" : pm->filename);
// in case people ignore the error...
vm_vec_zero(pos);
return;
}
vm_vec_zero(pos);
// we take a regular average, add them all up, divide by the total number, but weighted by how close they are to the z slice
float accum_scale_factor = 0.0f;
for (int i = 0; i < tree->n_verts; i++) {
// this goes from 1 directly at our z pos, and quickly goes to 0 the further it gets
float scale_factor = 1 / ((fabs(tree->point_list[i].xyz.z - z_slice_pos) / (pm->rad / 10)) + 1);
vm_vec_scale_add(pos, pos, &tree->point_list[i], scale_factor);
// keep track of the scale factor we use because we need to divide by its total at the end
accum_scale_factor += scale_factor;
}
*pos /= accum_scale_factor;
}
}
void submodel_get_cross_sectional_random_pos(int model_num, int submodel_num, float z_slice_pos, vec3d* pos)
{
polymodel* pm = model_get(model_num);
if (pm != nullptr) {
if (submodel_num < 0) {
submodel_num = pm->detail[0];
}
// the Shivan Comm Node does not have a collision tree, for one
if (pm->submodel[submodel_num].collision_tree_index < 0) {
nprintf(("Model", "In submodel_get_cross_sectional_random_pos(), model %s does not have a collision tree!\n", pm->filename));
return;
}
bsp_collision_tree* tree = model_get_bsp_collision_tree(pm->submodel[submodel_num].collision_tree_index);
int nv = tree->n_verts;
// this is not only because of the immediate div-0 error but also because of the less immediate expectation for at least one point (preferably two) to be found
if (nv <= 0) {
Error(LOCATION, "Model %d ('%s') must have at least one point from submodel_get_cross_sectional_random_pos!", model_num, (pm == nullptr) ? "<null model?!?>" : pm->filename);
// in case people ignore the error...
vm_vec_zero(pos);
return;
}
vm_vec_zero(pos);
vec3d best1, best2;
// make random guesses a bunch of times, and average our two best guesses closest to the z pos, ez
// there are more accurate ways, but this is reasonably good and super cheap
vm_vec_make(&best1, 0, 0, 999999);
vm_vec_make(&best2, 0, 0, 999999);
for (int i = 0; i < 15; i++) {
vec3d rand_point = tree->point_list[Random::next(nv)];
if (fabs(rand_point.xyz.z - z_slice_pos) < fabs(best1.xyz.z - z_slice_pos))
best1 = rand_point;
else if (fabs(rand_point.xyz.z - z_slice_pos) < fabs(best2.xyz.z - z_slice_pos))
best2 = rand_point;
}
vm_vec_avg(pos, &best1, &best2);
}
}
// If MR_FLAG_OUTLINE bit set this color will be used for outlines.
// This defaults to black.
void model_set_outline_color(int r, int g, int b )
{
gr_init_color( &Interp_outline_color, r, g, b );
}
// IF MR_LOCK_DETAIL is set, then it will always draw detail level 'n'
// This defaults to 0. (0=highest, larger=lower)
void model_set_detail_level(int n)
{
Interp_detail_level_locked = n;
}
/**
* Returns number of tmaps & flat polys in a submodel;
*/
int submodel_get_num_polys_sub( ubyte *p )
{
int chunk_type = w(p);
int chunk_size = w(p+4);
int n = 0;
bool end = chunk_type == OP_EOF;
while (!end) {
switch (chunk_type) {
case OP_DEFPOINTS: break;
case OP_FLATPOLY: n++; break;
case OP_TMAPPOLY: n++; break;
case OP_SORTNORM: {
int frontlist = w(p+36);
int backlist = w(p+40);
int prelist = w(p+44);
int postlist = w(p+48);
int onlist = w(p+52);
n += submodel_get_num_polys_sub(p+frontlist);
n += submodel_get_num_polys_sub(p+backlist);
n += submodel_get_num_polys_sub(p+prelist);
n += submodel_get_num_polys_sub(p+postlist );
n += submodel_get_num_polys_sub(p+onlist );
}
break;
case OP_SORTNORM2: {
int frontlist = w(p + 8);
int backlist = w(p + 12);
n += submodel_get_num_polys_sub(p + frontlist);
n += submodel_get_num_polys_sub(p + backlist);
}
end = true; // should not continue after this chunk
break;
case OP_BOUNDBOX: break;
case OP_TMAP2POLY:
n++;
end = true; // should not continue after this chunk
break;
default:
mprintf(( "Bad chunk type %d, len=%d in submodel_get_num_polys\n", chunk_type, chunk_size ));
Int3(); // Bad chunk type!
return 0;
}
if (end) break;
p += chunk_size;
chunk_type = w(p);
chunk_size = w(p+4);
if (chunk_type == OP_EOF)
end = true;
}
return n;
}
/**
* Returns number of tmaps & flat polys in a submodel
*/
int submodel_get_num_polys(int model_num, int submodel_num )
{
polymodel * pm;
pm = model_get(model_num);
return submodel_get_num_polys_sub( pm->submodel[submodel_num].bsp_data.get() );
}
/**
* See if the given texture is used by the passed model. 0 if not used, 1 if used, -1 on error
*/
int model_find_texture(int model_num, int bitmap)
{
polymodel * pm;
int idx;
// get a handle to the model
pm = model_get(model_num);
if(pm == NULL){
return -1;
}
// find the texture
for(idx=0; idx<pm->n_textures; idx++)
{
if(pm->maps[idx].FindTexture(bitmap) > -1)
{
return 1;
}
}
// no texture
return 0;
}
// find closest point on extended bounding box (the bounding box plus all the planes that make it up)
// returns closest distance to extended box
// positive return value means start_point is outside extended box
// displaces closest point an optional amount delta to the outside of the box
// closest_box_point can be NULL.
float get_model_closest_box_point_with_delta(vec3d *closest_box_point, const vec3d *start_point, int modelnum, int *is_inside, float delta)
{
int i, idx;
vec3d box_point, ray_direction, *extremes;
float dist, best_dist;
polymodel *pm;
int inside = 0;
int masks[6] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20};
int mask_inside = 0x3f;
best_dist = FLT_MAX;
pm = model_get(modelnum);
for (i=0; i<6; i++) {
idx = i / 2; // which row vector of Identity matrix
memcpy(&ray_direction, vmd_identity_matrix.a2d[idx], sizeof(vec3d));
// do negative, then positive plane for each axis
if (2 * idx == i) {
extremes = &pm->mins;
vm_vec_negate(&ray_direction);
} else {
extremes = &pm->maxs;
}
// a negative distance means started outside the box
dist = fvi_ray_plane(&box_point, extremes, &ray_direction, start_point, &ray_direction, 0.0f);
if (dist > 0) {
inside |= masks[i];
}
if (fabs(dist) < fabs(best_dist)) {
best_dist = dist;
if (closest_box_point) {
vm_vec_scale_add(closest_box_point, &box_point, &ray_direction, delta);
}
}
}
// is start_point inside the box
if (is_inside) {
*is_inside = (inside == mask_inside);
}
return -best_dist;
}
// find closest point on extended bounding box (the bounding box plus all the planes that make it up)
// returns closest distance to extended box
// positive return value means start_point is outside extended box
// displaces closest point an optional amount delta to the outside of the box
// closest_box_point can be NULL.
float get_world_closest_box_point_with_delta(vec3d *closest_box_point, const object *box_obj, const vec3d *start_point, int *is_inside, float delta)
{
vec3d temp, box_start;
float dist;
int modelnum;
// get modelnum
modelnum = Ship_info[Ships[box_obj->instance].ship_info_index].model_num;
// rotate start_point to box_obj RF
vm_vec_sub(&temp, start_point, &box_obj->pos);
vm_vec_rotate(&box_start, &temp, &box_obj->orient);
dist = get_model_closest_box_point_with_delta(closest_box_point, &box_start, modelnum, is_inside, delta);
// rotate closest_box_point to world RF
if (closest_box_point) {
vm_vec_unrotate(&temp, closest_box_point, &box_obj->orient);
vm_vec_add(closest_box_point, &temp, &box_obj->pos);
}
return dist;
}
/**
* Given a newly loaded model, page in all textures
*/
void model_page_in_textures(int modelnum, int ship_info_index)
{
int i, idx;
polymodel *pm = model_get(modelnum);
// bogus
if (pm == NULL)
return;
for (idx = 0; idx < pm->n_textures; idx++) {
pm->maps[idx].PageIn();
}
for (i = 0; i < pm->n_glow_point_banks; i++) {
glow_point_bank *bank = &pm->glow_point_banks[i];
bm_page_in_texture(bank->glow_bitmap);
bm_page_in_texture(bank->glow_neb_bitmap);
}
if (ship_info_index >= 0)
ship_page_in_textures(ship_info_index);
}
// unload all textures for a given model
// "release" should only be set if called from model_unload()!!!
void model_page_out_textures(int model_num, bool release)
{
if (model_num < 0)
return;
polymodel *pm = model_get(model_num);
if (pm == NULL)
return;
if (release && (pm->used_this_mission > 0))
return;
model_page_out_textures(pm, release);
}
void model_page_out_textures(polymodel* pm, bool release, const SCP_set<int>& skipTextures, const SCP_set<int>& skipGlowBanks)
{
int i, j;
for (i = 0; i < pm->n_textures; i++) {
if (skipTextures.contains(i))
continue;
pm->maps[i].PageOut(release);
}
// NOTE: "release" doesn't work here for some, as of yet unknown, reason - taylor
for (j = 0; j < pm->n_glow_point_banks; j++) {
if(skipGlowBanks.contains(j))
continue;
glow_point_bank* bank = &pm->glow_point_banks[j];
if (bank->glow_bitmap >= 0) {
// if (release) {
// bm_release(bank->glow_bitmap);
// bank->glow_bitmap = -1;
// } else {
bm_unload(bank->glow_bitmap);
// }
}
if (bank->glow_neb_bitmap >= 0) {
// if (release) {
// bm_release(bank->glow_neb_bitmap);
// bank->glow_neb_bitmap = -1;
// } else {
bm_unload(bank->glow_neb_bitmap);
// }
}
}
}
//**********vertex buffer stuff**********//
int tri_count[MAX_MODEL_TEXTURES];
poly_list polygon_list[MAX_MODEL_TEXTURES];
void parse_defpoint(int off, ubyte *bsp_data)
{
uint i, n;
uint nverts = uw(off+bsp_data+8);
uint offset = uw(off+bsp_data+16);
uint next_norm = 0;
ubyte *normcount = off+bsp_data+20;
vec3d *src = vp(off+bsp_data+offset);
// Get pointer to lights
Interp_lights = off+bsp_data+20+nverts;
#ifndef NDEBUG
modelstats_num_verts += nverts;
#endif
for (n = 0; n < nverts; n++) {
Interp_verts[n] = src;
src++; // move to normal
for (i = 0; i < normcount[n]; i++) {
Interp_norms[next_norm] = src;
next_norm++;
src++;
}
}
}
int check_values(vec3d *N)
{
// Values equal to -1.#IND0
if(!is_valid_vec(N))
{
N->xyz.x = 1.0f;
N->xyz.y = 0.0f;
N->xyz.z = 0.0f;
return 1;
}
return 0;
}
int Parse_normal_problem_count = 0;
void parse_tmap(int offset, ubyte *bsp_data)
{
int pof_tex = w(bsp_data+offset+TMAP_TEXNUM);
uint n_vert = uw(bsp_data+offset+TMAP_NVERTS);
ubyte *p = &bsp_data[offset+TMAP_NORMAL];
auto tverts = reinterpret_cast<model_tmap_vert_old*>(&bsp_data[offset + TMAP_VERTS]);
vertex *V;
vec3d *v;
vec3d *N;
int problem_count = 0;
for (uint i = 1; i < (n_vert-1); i++) {
V = &polygon_list[pof_tex].vert[(polygon_list[pof_tex].n_verts)];
N = &polygon_list[pof_tex].norm[(polygon_list[pof_tex].n_verts)];
v = Interp_verts[tverts[0].vertnum];
V->world.xyz.x = v->xyz.x;
V->world.xyz.y = v->xyz.y;
V->world.xyz.z = v->xyz.z;
V->texture_position.u = tverts[0].u;
V->texture_position.v = tverts[0].v;
*N = *Interp_norms[tverts[0].normnum];
if ( IS_VEC_NULL(N) )
*N = *vp(p);
problem_count += check_values(N);
vm_vec_normalize_safe(N);
V = &polygon_list[pof_tex].vert[(polygon_list[pof_tex].n_verts)+1];
N = &polygon_list[pof_tex].norm[(polygon_list[pof_tex].n_verts)+1];
v = Interp_verts[tverts[i].vertnum];
V->world.xyz.x = v->xyz.x;
V->world.xyz.y = v->xyz.y;
V->world.xyz.z = v->xyz.z;
V->texture_position.u = tverts[i].u;
V->texture_position.v = tverts[i].v;
*N = *Interp_norms[tverts[i].normnum];
if ( IS_VEC_NULL(N) )
*N = *vp(p);
problem_count += check_values(N);
vm_vec_normalize_safe(N);
V = &polygon_list[pof_tex].vert[(polygon_list[pof_tex].n_verts)+2];
N = &polygon_list[pof_tex].norm[(polygon_list[pof_tex].n_verts)+2];
v = Interp_verts[tverts[i+1].vertnum];
V->world.xyz.x = v->xyz.x;
V->world.xyz.y = v->xyz.y;
V->world.xyz.z = v->xyz.z;
V->texture_position.u = tverts[i+1].u;
V->texture_position.v = tverts[i+1].v;
*N = *Interp_norms[tverts[i+1].normnum];
if ( IS_VEC_NULL(N) )
*N = *vp(p);
problem_count += check_values(N);
vm_vec_normalize_safe(N);
polygon_list[pof_tex].n_verts += 3;
}
Parse_normal_problem_count += problem_count;
}
/**
* @brief Parses a TMAP2POLY chunk into a list of polygons.
*
* @param offset The byte offset to the current TMAP2POLY chunk within bsp_data.
* @param[in] bsp_data The byte buffer containing the BSP information for the current model.
*/
void parse_tmap2(int offset, ubyte* bsp_data)
{
int pof_tex = w(bsp_data + offset + TMAP2_TEXNUM);
uint n_vert = uw(bsp_data + offset + TMAP2_NVERTS);
ubyte* p = &bsp_data[offset + TMAP2_NORMAL];
model_tmap_vert* tverts;
vertex* V;
vec3d* v;
vec3d* N;
int problem_count = 0;
tverts = reinterpret_cast<model_tmap_vert*>(&bsp_data[offset + TMAP2_VERTS]);
for (uint i = 1; i < (n_vert - 1); i++) {
V = &polygon_list[pof_tex].vert[(polygon_list[pof_tex].n_verts)];
N = &polygon_list[pof_tex].norm[(polygon_list[pof_tex].n_verts)];
v = Interp_verts[tverts[0].vertnum];
V->world.xyz.x = v->xyz.x;
V->world.xyz.y = v->xyz.y;
V->world.xyz.z = v->xyz.z;
V->texture_position.u = tverts[0].u;
V->texture_position.v = tverts[0].v;
*N = *Interp_norms[tverts[0].normnum];
if (IS_VEC_NULL(N))
*N = *vp(p);
problem_count += check_values(N);
vm_vec_normalize_safe(N);
V = &polygon_list[pof_tex].vert[(polygon_list[pof_tex].n_verts) + 1];
N = &polygon_list[pof_tex].norm[(polygon_list[pof_tex].n_verts) + 1];
v = Interp_verts[tverts[i].vertnum];
V->world.xyz.x = v->xyz.x;
V->world.xyz.y = v->xyz.y;
V->world.xyz.z = v->xyz.z;
V->texture_position.u = tverts[i].u;
V->texture_position.v = tverts[i].v;
*N = *Interp_norms[tverts[i].normnum];
if (IS_VEC_NULL(N))
*N = *vp(p);
problem_count += check_values(N);
vm_vec_normalize_safe(N);
V = &polygon_list[pof_tex].vert[(polygon_list[pof_tex].n_verts) + 2];
N = &polygon_list[pof_tex].norm[(polygon_list[pof_tex].n_verts) + 2];
v = Interp_verts[tverts[i + 1].vertnum];
V->world.xyz.x = v->xyz.x;
V->world.xyz.y = v->xyz.y;
V->world.xyz.z = v->xyz.z;
V->texture_position.u = tverts[i + 1].u;
V->texture_position.v = tverts[i + 1].v;
*N = *Interp_norms[tverts[i + 1].normnum];
if (IS_VEC_NULL(N))
*N = *vp(p);
problem_count += check_values(N);
vm_vec_normalize_safe(N);
polygon_list[pof_tex].n_verts += 3;
}
Parse_normal_problem_count += problem_count;
}
void parse_bsp(int offset, ubyte* bsp_data);
void parse_sortnorm(int offset, ubyte* bsp_data)
{
int frontlist, backlist, prelist, postlist, onlist;
frontlist = w(bsp_data + offset + 36);
backlist = w(bsp_data + offset + 40);
prelist = w(bsp_data + offset + 44);
postlist = w(bsp_data + offset + 48);
onlist = w(bsp_data + offset + 52);
if (prelist) parse_bsp(offset + prelist, bsp_data);
if (backlist) parse_bsp(offset + backlist, bsp_data);
if (onlist) parse_bsp(offset + onlist, bsp_data);
if (frontlist) parse_bsp(offset + frontlist, bsp_data);
if (postlist) parse_bsp(offset + postlist, bsp_data);
}
/**
* @brief Parses a SORTNORM2 by recursively parsing into the two pointers it contains.
*
* @param offset The byte offset to the current SORT2NORM chunk within bsp_data.
* @param bsp_data The byte buffer containing the BSP information for the current model.
*/
void parse_sortnorm2(int offset, ubyte* bsp_data)
{
int frontlist, backlist;
frontlist = w(bsp_data + offset + 8);
backlist = w(bsp_data + offset + 12);
if (backlist) parse_bsp(offset + backlist, bsp_data);
if (frontlist) parse_bsp(offset + frontlist, bsp_data);
}
void parse_bsp(int offset, ubyte *bsp_data)
{
int id = w(bsp_data+offset);
int size = w(bsp_data+offset+4);
bool end = id == OP_EOF;
while (!end) {
switch (id)
{
case OP_DEFPOINTS:
parse_defpoint(offset, bsp_data);
break;
case OP_SORTNORM:
parse_sortnorm(offset, bsp_data);
break;
case OP_SORTNORM2:
parse_sortnorm2(offset, bsp_data);
end = true; // should not continue after this chunk
break;
case OP_FLATPOLY:
break;
case OP_TMAPPOLY:
parse_tmap(offset, bsp_data);
break;
case OP_BOUNDBOX:
break;
case OP_TMAP2POLY:
parse_tmap2(offset, bsp_data);
end = true; // should not continue after this chunk
break;
default:
return;
}
if (end) break;
offset += size;
id = w(bsp_data+offset);
size = w(bsp_data+offset+4);
if (size < 1 || id == OP_EOF)
end = true;
}
}
void find_tmap(int offset, const ubyte *bsp_data, int id)
{
int pof_tex = cw(bsp_data+offset+(id == OP_TMAP2POLY ? TMAP2_TEXNUM : TMAP_TEXNUM));
uint n_vert = cuw(bsp_data+offset+ (id == OP_TMAP2POLY ? TMAP2_NVERTS : TMAP_NVERTS));
tri_count[pof_tex] += n_vert-2;
}
void find_defpoint(int off, ubyte *bsp_data)
{
uint n;
uint nverts = uw(off+bsp_data+8);
ubyte * normcount = off+bsp_data+20;
// Get pointer to lights
Interp_lights = off+bsp_data+20+nverts;
#ifndef NDEBUG
modelstats_num_verts += nverts;
#endif
int norm_num = 0;
for (n = 0; n < nverts; n++) {
norm_num += normcount[n];
}
Interp_num_verts = nverts;
Interp_num_norms = norm_num;
}
void find_tri_counts(int offset, ubyte* bsp_data);
void find_sortnorm(int offset, ubyte* bsp_data)
{
int frontlist, backlist, prelist, postlist, onlist;
frontlist = w(bsp_data + offset + 36);
backlist = w(bsp_data + offset + 40);
prelist = w(bsp_data + offset + 44);
postlist = w(bsp_data + offset + 48);
onlist = w(bsp_data + offset + 52);
if (prelist) find_tri_counts(offset + prelist, bsp_data);
if (backlist) find_tri_counts(offset + backlist, bsp_data);
if (onlist) find_tri_counts(offset + onlist, bsp_data);
if (frontlist) find_tri_counts(offset + frontlist, bsp_data);
if (postlist) find_tri_counts(offset + postlist, bsp_data);
}
void find_sortnorm2(int offset, ubyte* bsp_data)
{
int frontlist, backlist;
frontlist = w(bsp_data + offset + 8);
backlist = w(bsp_data + offset + 12);
if (backlist) find_tri_counts(offset + backlist, bsp_data);
if (frontlist) find_tri_counts(offset + frontlist, bsp_data);
}
// tri_count
void find_tri_counts(int offset, ubyte *bsp_data)
{
int id = w(bsp_data+offset);
int size = w(bsp_data+offset+4);
bool end = id == OP_EOF;
while (!end) {
switch (id)
{
case OP_DEFPOINTS:
find_defpoint(offset, bsp_data);
break;
case OP_SORTNORM:
find_sortnorm(offset, bsp_data);
break;
case OP_SORTNORM2:
find_sortnorm2(offset, bsp_data);
end = true; // should not continue after this chunk
break;
case OP_FLATPOLY:
break;
case OP_TMAPPOLY:
find_tmap(offset, bsp_data, id);
break;
case OP_TMAP2POLY:
find_tmap(offset, bsp_data, id);
end = true; // should not continue after this chunk
break;
case OP_BOUNDBOX:
break;
default:
return;
}
if (end) break;
offset += size;
id = w(bsp_data+offset);
size = w(bsp_data+offset+4);
if (size < 1 || id == OP_EOF)
end = true;
}
}
void model_interp_submit_buffers(indexed_vertex_source *vert_src, size_t vertex_stride)
{
Assert(vert_src != NULL);
if ( !(vert_src->Vertex_list_size > 0 && vert_src->Index_list_size > 0 ) ) {
return;
}
if ( vert_src->Vertex_list != nullptr ) {
size_t offset;
gr_heap_allocate(GpuHeap::ModelVertex, vert_src->Vertex_list_size, vert_src->Vertex_list.get(), offset, vert_src->Vbuffer_handle);
// If this happens then someone must have allocated something from the heap with a different stride than what we
// are using.
Assertion(offset % vertex_stride == 0, "Offset returned by GPU heap allocation does not match stride value!");
vert_src->Base_vertex_offset = offset / vertex_stride;
vert_src->Vertex_offset = offset;
vert_src->Vertex_list.reset();
}
if ( vert_src->Index_list != nullptr ) {
gr_heap_allocate(GpuHeap::ModelIndex, vert_src->Index_list_size, vert_src->Index_list.get(), vert_src->Index_offset, vert_src->Ibuffer_handle);
vert_src->Index_list.reset();
}
}
bool model_interp_pack_buffer(indexed_vertex_source *vert_src, vertex_buffer *vb)
{
if ( vert_src == NULL ) {
return false;
}
Assertion(vb != nullptr, "Invalid vertex buffer specified!");
int i, n_verts = 0;
size_t j;
if ( vert_src->Vertex_list == nullptr ) {
vert_src->Vertex_list = make_shared<uint8_t[]>(vert_src->Vertex_list_size);
// return invalid if we don't have the memory
if ( vert_src->Vertex_list == nullptr ) {
return false;
}
}
if ( vert_src->Index_list == nullptr ) {
vert_src->Index_list = make_shared<uint8_t[]>(vert_src->Index_list_size);
// return invalid if we don't have the memory
if ( vert_src->Index_list == nullptr ) {
return false;
}
}
// bump to our index in the array
auto array = reinterpret_cast<interp_vertex*>(vert_src->Vertex_list.get() + vb->vertex_offset);
// generate the vertex array
n_verts = vb->model_list->n_verts;
for ( i = 0; i < n_verts; i++ ) {
vertex *vl = &vb->model_list->vert[i];
auto outVert = &array[i];
// don't try to generate more data than what's available
Assert(((i * sizeof(interp_vertex)) + sizeof(interp_vertex)) <= (vert_src->Vertex_list_size - vb->vertex_offset));
// NOTE: UV->NORM->TSB->MODEL_ID->VERT, This array order *must* be preserved!!
// tex coords
if ( vb->flags & VB_FLAG_UV1 ) {
outVert->uv = vl->texture_position;
} else {
outVert->uv.u = 1.0f;
outVert->uv.v = 1.0f;
}
// normals
if ( vb->flags & VB_FLAG_NORMAL ) {
Assert(vb->model_list->norm != NULL);
outVert->normal = vb->model_list->norm[i];
} else {
outVert->normal.xyz.x = 0.0f;
outVert->normal.xyz.y = 0.0f;
outVert->normal.xyz.z = 1.0f;
}
// tangent space data
if ( vb->flags & VB_FLAG_TANGENT ) {
Assert(vb->model_list->tsb != NULL);
tsb_t *tsb = &vb->model_list->tsb[i];
outVert->tangent.xyzw.x = tsb->tangent.xyz.x;
outVert->tangent.xyzw.y = tsb->tangent.xyz.y;
outVert->tangent.xyzw.z = tsb->tangent.xyz.z;
outVert->tangent.xyzw.w = tsb->scaler;
} else {
outVert->tangent.xyzw.x = 1.0f;
outVert->tangent.xyzw.y = 0.0f;
outVert->tangent.xyzw.z = 0.0f;
outVert->tangent.xyzw.w = 0.0f;
}
if ( vb->flags & VB_FLAG_MODEL_ID ) {
Assert(vb->model_list->submodels != NULL);
outVert->modelId = (float)vb->model_list->submodels[i];
} else {
outVert->modelId = 0.0f;
}
// verts
outVert->pos = vl->world;
}
// generate the index array
for ( j = 0; j < vb->tex_buf.size(); j++ ) {
buffer_data* tex_buf = &vb->tex_buf[j];
n_verts = (int)tex_buf->n_verts;
auto offset = tex_buf->index_offset;
const uint *index = tex_buf->get_index();
// bump to our spot in the buffer
auto ibuf = vert_src->Index_list.get() + offset;
if ( vb->tex_buf[j].flags & VB_FLAG_LARGE_INDEX ) {
memcpy(ibuf, index, n_verts * sizeof(uint));
} else {
ushort *mybuf = (ushort*)ibuf;
for ( i = 0; i < n_verts; i++ ) {
mybuf[i] = (ushort)index[i];
}
}
}
return true;
}
void interp_pack_vertex_buffers(polymodel *pm, int mn)
{
Assert( (mn >= 0) && (mn < pm->n_models) );
bsp_info *model = &pm->submodel[mn];
if ( !model->buffer.model_list ) {
return;
}
bool rval = model_interp_pack_buffer(&pm->vert_source, &model->buffer);
if ( model->trans_buffer.flags & VB_FLAG_TRANS && !model->trans_buffer.tex_buf.empty() ) {
model_interp_pack_buffer(&pm->vert_source, &model->trans_buffer);
}
if ( !rval ) {
Error( LOCATION, "Unable to pack vertex buffer for '%s'\n", pm->filename );
}
}
void model_interp_set_buffer_layout(vertex_layout *layout)
{
Assert(layout != NULL);
// Similarly to model_interp_config_buffer, we add all vectex components even if they aren't used
// This reduces the amount of vertex format respecification and since the data contains valid data there is no risk
// of reading garbage data on the GPU
layout->add_vertex_component(vertex_format_data::TEX_COORD2, sizeof(interp_vertex), offsetof(interp_vertex, uv));
layout->add_vertex_component(vertex_format_data::NORMAL, sizeof(interp_vertex), offsetof(interp_vertex, normal));
layout->add_vertex_component(vertex_format_data::TANGENT, sizeof(interp_vertex), offsetof(interp_vertex, tangent));
layout->add_vertex_component(vertex_format_data::MODEL_ID, sizeof(interp_vertex), offsetof(interp_vertex, modelId));
layout->add_vertex_component(vertex_format_data::POSITION3, sizeof(interp_vertex), offsetof(interp_vertex, pos));
}
bool model_interp_config_buffer(indexed_vertex_source *vert_src, vertex_buffer *vb, bool update_ibuffer_only)
{
if ( vb == NULL ) {
return false;
}
if ( !(vb->flags & VB_FLAG_POSITION) ) {
Int3();
return false;
}
// pad out the vertex buffer even if it doesn't use certain attributes
// we require consistent stride across vertex buffers so we can use base vertex offsetting for performance reasons
vb->stride = sizeof(interp_vertex);
model_interp_set_buffer_layout(&vb->layout);
// offsets for this chunk
if ( !update_ibuffer_only ) {
vb->vertex_offset = vert_src->Vertex_list_size;
vb->vertex_num_offset = vb->vertex_offset / vb->stride;
vert_src->Vertex_list_size += (uint)(vb->stride * vb->model_list->n_verts);
}
for ( size_t idx = 0; idx < vb->tex_buf.size(); idx++ ) {
buffer_data *bd = &vb->tex_buf[idx];
bd->index_offset = vert_src->Index_list_size;
vert_src->Index_list_size += (uint)(bd->n_verts * ((bd->flags & VB_FLAG_LARGE_INDEX) ? sizeof(uint) : sizeof(ushort)));
// even out index buffer so we are always word aligned
vert_src->Index_list_size += (uint)(vert_src->Index_list_size % sizeof(uint));
}
return true;
}
void interp_configure_vertex_buffers(polymodel *pm, int mn, const model_read_deferred_tasks& deferredTasks)
{
TRACE_SCOPE(tracing::ModelConfigureVertexBuffers);
int i, j, first_index;
uint total_verts = 0;
SCP_vector<int> vertex_list;
Assert( (mn >= 0) && (mn < pm->n_models) );
bsp_info *model = &pm->submodel[mn];
for (i = 0; i < MAX_MODEL_TEXTURES; i++) {
polygon_list[i].n_verts = 0;
tri_count[i] = 0;
}
int milliseconds = timer_get_milliseconds();
auto bsp_polies = new bsp_polygon_data(model->bsp_data.get(), model->bsp_data_size);
auto textureReplace = deferredTasks.texture_replacements.find(mn);
if (textureReplace != deferredTasks.texture_replacements.end())
bsp_polies->replace_textures_used(textureReplace->second.replacementIds);
for (i = 0; i < MAX_MODEL_TEXTURES; i++) {
int vert_count = bsp_polies->get_num_triangles(i) * 3;
tri_count[i] = vert_count / 3;
total_verts += vert_count;
polygon_list[i].allocate(vert_count);
bsp_polies->generate_triangles(i, polygon_list[i].vert, polygon_list[i].norm);
polygon_list[i].n_verts = vert_count;
// set submodel ID
for ( j = 0; j < polygon_list[i].n_verts; ++j ) {
polygon_list[i].submodels[j] = mn;
}
// for the moment we can only support INT_MAX worth of verts per index buffer
if (total_verts > INT_MAX) {
Error( LOCATION, "Unable to generate vertex buffer data because model '%s' with %i verts is over the maximum of %i verts!\n", pm->filename, total_verts, INT_MAX);
}
}
// figure out if we have an outline
int outline_n_lines = bsp_polies->get_num_lines(-1);
if ( outline_n_lines > 0 ) {
model->n_verts_outline = outline_n_lines * 2;
model->outline_buffer = make_shared<vertex[]>(model->n_verts_outline);
bsp_polies->generate_lines(-1, model->outline_buffer.get());
}
// done with the bsp now that we have the vertex data
delete bsp_polies;
int time_elapsed = timer_get_milliseconds() - milliseconds;
nprintf(("Model", "BSP Parse took %d milliseconds.\n", time_elapsed));
if (total_verts < 1) {
return;
}
total_verts = 0;
for (i = 0; i < MAX_MODEL_TEXTURES; i++) {
total_verts += polygon_list[i].n_verts;
}
poly_list *model_list = new(std::nothrow) poly_list;
if ( !model_list ) {
Error( LOCATION, "Unable to allocate memory for poly_list!\n" );
}
model->buffer.model_list = model_list;
model_list->allocate( (int)total_verts );
for (i = 0; i < MAX_MODEL_TEXTURES; i++) {
if ( !polygon_list[i].n_verts )
continue;
memcpy( (model_list->vert) + model_list->n_verts, polygon_list[i].vert, sizeof(vertex) * polygon_list[i].n_verts );
memcpy( (model_list->norm) + model_list->n_verts, polygon_list[i].norm, sizeof(vec3d) * polygon_list[i].n_verts );
if (Cmdline_normal) {
memcpy( (model_list->tsb) + model_list->n_verts, polygon_list[i].tsb, sizeof(tsb_t) * polygon_list[i].n_verts );
}
memcpy( (model_list->submodels) + model_list->n_verts, polygon_list[i].submodels, sizeof(int) * polygon_list[i].n_verts );
model_list->n_verts += polygon_list[i].n_verts;
}
// no read file so we'll have to generate
model_list->make_index_buffer(vertex_list);
vertex_list.clear(); // done
int vertex_flags = (VB_FLAG_POSITION | VB_FLAG_NORMAL | VB_FLAG_UV1);
if (model_list->tsb != NULL) {
Assert( Cmdline_normal );
vertex_flags |= VB_FLAG_TANGENT;
}
if ( model_list->submodels != NULL ) {
vertex_flags |= VB_FLAG_MODEL_ID;
}
model->buffer.flags = vertex_flags;
for (i = 0; i < MAX_MODEL_TEXTURES; i++) {
if ( !polygon_list[i].n_verts )
continue;
buffer_data new_buffer(polygon_list[i].n_verts);
Verify( new_buffer.get_index() != NULL );
for (j = 0; j < polygon_list[i].n_verts; j++) {
first_index = model_list->find_index_fast(&polygon_list[i], j);
Assert(first_index != -1);
new_buffer.assign(j, first_index);
}
new_buffer.texture = i;
new_buffer.flags = 0;
if (polygon_list[i].n_verts >= USHRT_MAX) {
new_buffer.flags |= VB_FLAG_LARGE_INDEX;
}
model->buffer.tex_buf.push_back( new_buffer );
}
bool rval = model_interp_config_buffer(&pm->vert_source, &model->buffer, false);
if ( !rval ) {
Error( LOCATION, "Unable to configure vertex buffer for '%s'\n", pm->filename );
}
}
void interp_copy_index_buffer(vertex_buffer *src, vertex_buffer *dest, size_t *index_counts)
{
size_t i, j, k;
size_t src_buff_size;
buffer_data *src_buffer;
buffer_data *dest_buffer;
size_t vert_offset = src->vertex_num_offset; // assuming all submodels crunched into this index buffer have the same stride
//int vert_offset = 0;
for ( i = 0; i < dest->tex_buf.size(); ++i ) {
dest_buffer = &dest->tex_buf[i];
for ( j = 0; j < src->tex_buf.size(); ++j ) {
if ( dest_buffer->texture != src->tex_buf[j].texture ) {
continue;
}
src_buffer = &src->tex_buf[j];
src_buff_size = (size_t)src_buffer->n_verts;
for ( k = 0; k < src_buff_size; ++k ) {
dest_buffer->assign(dest_buffer->n_verts, (uint32_t)(src_buffer->get_index()[k] + vert_offset)); // take into account the vertex offset.
dest_buffer->n_verts++;
Assert(dest_buffer->n_verts <= index_counts[dest_buffer->texture]);
}
}
}
}
void interp_fill_detail_index_buffer(SCP_vector<int> &submodel_list, polymodel *pm, vertex_buffer *buffer)
{
size_t index_counts[MAX_MODEL_TEXTURES];
int i, j;
int model_num;
for ( i = 0; i < MAX_MODEL_TEXTURES; ++i ) {
index_counts[i] = 0;
}
buffer->vertex_offset = 0;
buffer->vertex_num_offset = 0;
buffer->model_list = new(std::nothrow) poly_list;
int num_buffers;
int tex_num;
// need to first count how many indexes there are in this entire detail model hierarchy
for ( i = 0; i < (int)submodel_list.size(); ++i ) {
model_num = submodel_list[i];
if ( pm->submodel[model_num].flags[Model::Submodel_flags::Is_thruster] ) {
continue;
}
num_buffers = (int)pm->submodel[model_num].buffer.tex_buf.size();
buffer->flags |= pm->submodel[model_num].buffer.flags;
for ( j = 0; j < num_buffers; ++j ) {
tex_num = pm->submodel[model_num].buffer.tex_buf[j].texture;
index_counts[tex_num] += pm->submodel[model_num].buffer.tex_buf[j].n_verts;
}
}
// allocate the respective texture buffers with indexes for our detail buffer
for ( i = 0; i < MAX_MODEL_TEXTURES; ++i ) {
if ( index_counts[i] == 0 ) {
continue;
}
buffer->tex_buf.push_back(buffer_data(index_counts[i]));
buffer_data &new_buffer = buffer->tex_buf.back();
//new_buffer.n_verts = 0;
new_buffer.texture = i;
}
for ( i = 0; i < (int)buffer->tex_buf.size(); ++i ) {
buffer->tex_buf[i].n_verts = 0;
}
// finally copy over the indexes
for ( i = 0; i < (int)submodel_list.size(); ++i ) {
model_num = submodel_list[i];
if (pm->submodel[model_num].flags[Model::Submodel_flags::Is_thruster]) {
continue;
}
interp_copy_index_buffer(&pm->submodel[model_num].buffer, buffer, index_counts);
}
// check which buffers need to have the > USHORT flag
for ( i = 0; i < (int)buffer->tex_buf.size(); ++i ) {
if ( buffer->tex_buf[i].i_last >= USHRT_MAX ) {
buffer->tex_buf[i].flags |= VB_FLAG_LARGE_INDEX;
}
}
}
void interp_create_detail_index_buffer(polymodel *pm, int detail_num)
{
TRACE_SCOPE(tracing::ModelCreateDetailIndexBuffers);
SCP_vector<int> submodel_list;
submodel_list.clear();
model_get_submodel_tree_list(submodel_list, pm, pm->detail[detail_num]);
if ( submodel_list.empty() ) {
return;
}
interp_fill_detail_index_buffer(submodel_list, pm, &pm->detail_buffers[detail_num]);
// check if anything was even put into this buffer
if ( pm->detail_buffers[detail_num].tex_buf.empty() ) {
return;
}
model_interp_config_buffer(&pm->vert_source, &pm->detail_buffers[detail_num], true);
}
void interp_create_transparency_index_buffer(polymodel *pm, int mn)
{
TRACE_SCOPE(tracing::ModelCreateTransparencyIndexBuffer);
const int NUM_VERTS_PER_TRI = 3;
bsp_info *sub_model = &pm->submodel[mn];
vertex_buffer *trans_buffer = &sub_model->trans_buffer;
trans_buffer->model_list = new(std::nothrow) poly_list;
trans_buffer->vertex_offset = pm->submodel[mn].buffer.vertex_offset;
trans_buffer->vertex_num_offset = pm->submodel[mn].buffer.vertex_num_offset;
trans_buffer->stride = pm->submodel[mn].buffer.stride;
trans_buffer->flags = pm->submodel[mn].buffer.flags;
poly_list *model_list = pm->submodel[mn].buffer.model_list;
// bail out if this buffer is empty
if ( model_list == NULL || model_list->n_verts < 1 ) {
return;
}
SCP_vector<buffer_data> &tex_buffers = pm->submodel[mn].buffer.tex_buf;
uint current_tri[NUM_VERTS_PER_TRI];
bool transparent_tri = false;
int num_tris = 0;
for ( int i = 0; i < (int)tex_buffers.size(); ++i ) {
buffer_data *tex_buf = &tex_buffers[i];
if ( tex_buf->n_verts < 1 ) {
continue;
}
const uint *indices = tex_buf->get_index();
texture_map *tmap = &pm->maps[tex_buf->texture];
// skip if this is already designated to be a transparent pass by the modeller
// if ( tmap->is_transparent ) {
// continue;
// }
int bitmap_handle = tmap->textures[TM_BASE_TYPE].GetTexture();
if ( bitmap_handle < 0 || !bm_has_alpha_channel(bitmap_handle) ) {
continue;
}
bitmap_lookup texture_lookup(bitmap_handle);
if ( !texture_lookup.valid() ) {
continue;
}
SCP_vector<int> transparent_indices;
transparent_tri = false;
num_tris = 0;
for ( size_t j = 0; j < tex_buf->n_verts; ++j ) {
uint index = indices[j];
// need the uv coords of the vert at this index
float u = model_list->vert[index].texture_position.u;
float v = model_list->vert[index].texture_position.v;
if ( texture_lookup.get_channel_alpha(u, v) < 0.95f) {
transparent_tri = true;
}
current_tri[num_tris] = index;
num_tris++;
if ( num_tris == NUM_VERTS_PER_TRI ) {
if ( transparent_tri ) {
// we have a triangle and it's transparent.
// shove index into the transparency buffer
transparent_indices.push_back(current_tri[0]);
transparent_indices.push_back(current_tri[1]);
transparent_indices.push_back(current_tri[2]);
}
transparent_tri = false;
num_tris = 0;
}
}
if ( transparent_indices.empty() ) {
continue;
}
pm->flags |= PM_FLAG_TRANS_BUFFER;
trans_buffer->flags |= VB_FLAG_TRANS;
trans_buffer->tex_buf.push_back ( buffer_data ( transparent_indices.size() ) );
buffer_data &new_buff = trans_buffer->tex_buf.back();
new_buff.texture = tex_buf->texture;
for ( int j = 0; j < (int)transparent_indices.size(); ++j ) {
new_buff.assign(j, transparent_indices[j]);
}
}
if ( trans_buffer->flags & VB_FLAG_TRANS ) {
model_interp_config_buffer(&pm->vert_source, trans_buffer, true);
}
}
void model_interp_process_shield_mesh(polymodel * pm)
{
SCP_vector<vec3d> buffer;
if ( pm->shield.nverts <= 0 ) {
return;
}
int n_verts = 0;
for ( int i = 0; i < pm->shield.ntris; i++ ) {
shield_tri *tri = &pm->shield.tris[i];
vec3d a = pm->shield.verts[tri->verts[0]].pos;
vec3d b = pm->shield.verts[tri->verts[1]].pos;
vec3d c = pm->shield.verts[tri->verts[2]].pos;
// recalculate triangle normals to solve some issues regarding triangle collision
vec3d b_a;
vec3d c_a;
vm_vec_sub(&b_a, &b, &a);
vm_vec_sub(&c_a, &c, &a);
vm_vec_cross(&tri->norm, &b_a, &c_a);
vm_vec_normalize_safe(&tri->norm);
buffer.push_back(a);
buffer.push_back(tri->norm);
buffer.push_back(b);
buffer.push_back(tri->norm);
buffer.push_back(c);
buffer.push_back(tri->norm);
n_verts += 3;
}
if ( !buffer.empty() ) {
if (*pm->shield.buffer_id == gr_buffer_handle::invalid()) {
*pm->shield.buffer_id = gr_create_buffer(BufferType::Vertex, BufferUsageHint::Static);
pm->shield.buffer_n_verts = n_verts;
gr_update_buffer_data(*pm->shield.buffer_id, buffer.size() * sizeof(vec3d), buffer.data());
pm->shield.layout.add_vertex_component(vertex_format_data::POSITION3, sizeof(vec3d) * 2, 0);
pm->shield.layout.add_vertex_component(vertex_format_data::NORMAL, sizeof(vec3d) * 2, sizeof(vec3d));
}
} else {
*pm->shield.buffer_id = gr_buffer_handle::invalid();
}
}
// returns 1 if the thruster should be drawn
// 0 if it shouldn't
int model_should_render_engine_glow(int objnum, int bank_obj)
{
if ((bank_obj <= -1) || (objnum <= -1))
return 1;
object *obj = &Objects[objnum];
if (obj->type == OBJ_SHIP) {
ship_subsys *ssp;
ship *shipp = &Ships[obj->instance];
ship_info *sip = &Ship_info[shipp->ship_info_index];
Assert( bank_obj < sip->n_subsystems );
char subname[MAX_NAME_LEN];
// shipp->subsystems isn't always valid here so don't use it
strcpy_s(subname, sip->subsystems[bank_obj].subobj_name);
ssp = GET_FIRST(&shipp->subsys_list);
while ( ssp != END_OF_LIST( &shipp->subsys_list ) ) {
if ( !strcmp(subname, ssp->system_info->subobj_name) ) {
// this subsystem has 0 or less hits, ie. it's destroyed
if ( ssp->current_hits <= 0 )
return 0;
// see if the subsystem is disrupted, in which case it should be inoperable
if ( ship_subsys_disrupted(ssp) )
return 0;
return 1;
}
ssp = GET_NEXT( ssp );
}
} else if (obj->type == OBJ_WEAPON) {
// for weapons, if needed in the future
}
// default to render glow
return 1;
}
// Goober5000
// uses same algorithms as in ship_do_thruster_frame
int model_interp_get_texture(const texture_info *tinfo, int elapsed_time)
{
int texture, frame, cur_time, num_frames;
float total_time;
// get texture
num_frames = tinfo->GetNumFrames();
texture = tinfo->GetTexture();
total_time = tinfo->GetTotalTime();
// maybe animate it
if (texture >= 0 && num_frames > 1)
{
// sanity check total_time first thing
Assert(total_time > 0.0f);
total_time *= MILLISECONDS_PER_SECOND;
cur_time = elapsed_time % fl2i(total_time);
// get animation frame
frame = fl2i((cur_time * num_frames) / total_time + 0.5f);
CLAMP(frame, 0, num_frames - 1);
// advance to the correct frame
texture += frame;
}
// done
return texture;
}
void model_mix_two_team_colors(team_color* dest, team_color* a, team_color* b, float mix_factor)
{
dest->base.r = a->base.r * (1.0f - mix_factor) + b->base.r * mix_factor;
dest->base.g = a->base.g * (1.0f - mix_factor) + b->base.g * mix_factor;
dest->base.b = a->base.b * (1.0f - mix_factor) + b->base.b * mix_factor;
dest->stripe.r = a->stripe.r * (1.0f - mix_factor) + b->stripe.r * mix_factor;
dest->stripe.g = a->stripe.g * (1.0f - mix_factor) + b->stripe.g * mix_factor;
dest->stripe.b = a->stripe.b * (1.0f - mix_factor) + b->stripe.b * mix_factor;
}
bool model_get_team_color( team_color *clr, const SCP_string &team, const SCP_string &secondaryteam, fix timestamp, int fadetime )
{
Assert(clr != NULL);
if ( !stricmp(secondaryteam.c_str(), "none") ) {
if (Team_Colors.find(team) != Team_Colors.end()) {
*clr = Team_Colors[team];
return true;
} else
return false;
} else {
if ( Team_Colors.find(secondaryteam) != Team_Colors.end()) {
team_color temp_color;
team_color start;
if (Team_Colors.find(team) != Team_Colors.end()) {
start = Team_Colors[team];
} else {
start.base.r = 0.0f;
start.base.g = 0.0f;
start.base.b = 0.0f;
start.stripe.r = 0.0f;
start.stripe.g = 0.0f;
start.stripe.b = 0.0f;
}
team_color end = Team_Colors[secondaryteam];
float time_remaining = 0.0f;
if (fadetime != 0) // avoid potential div-by-zero
time_remaining = (f2fl(Missiontime - timestamp) * 1000)/fadetime;
CLAMP(time_remaining, 0.0f, 1.0f);
model_mix_two_team_colors(&temp_color, &start, &end, time_remaining);
*clr = temp_color;
return true;
} else
return false;
}
}
//********************-----CLASS: texture_info-----********************//
texture_info::texture_info()
{
clear();
}
texture_info::texture_info(int bm_handle)
{
if(!bm_is_valid(bm_handle))
{
clear();
return;
}
this->original_texture = bm_handle;
this->ResetTexture();
}
void texture_info::clear()
{
texture = original_texture = -1;
num_frames = 0;
total_time = 1.0f;
}
int texture_info::GetNumFrames() const
{
return num_frames;
}
int texture_info::GetOriginalTexture() const
{
return original_texture;
}
int texture_info::GetTexture() const
{
return texture;
}
float texture_info::GetTotalTime() const
{
return total_time;
}
int texture_info::LoadTexture(const char *filename, const char *dbg_name)
{
if (strlen(filename) + 4 >= NAME_LENGTH) //Filenames are passed in without extension
{
mprintf(("Generated texture name %s is too long. Skipping...\n", filename));
return -1;
}
this->original_texture = bm_load_either(filename, NULL, NULL, NULL, true, CF_TYPE_MAPS);
if(this->original_texture < 0)
nprintf(("Maps", "For \"%s\" I couldn't find %s.ani\n", dbg_name, filename));
this->ResetTexture();
return texture;
}
void texture_info::PageIn()
{
bm_page_in_texture(texture);
}
void texture_info::PageOut(bool release)
{
if (texture >= 0) {
if (release) {
bm_release(texture);
texture = -1;
num_frames = 0;
total_time = 1.0f;
} else {
bm_unload(texture);
}
}
}
int texture_info::ResetTexture()
{
return this->SetTexture(original_texture);
}
int texture_info::SetTexture(int n_tex)
{
if(n_tex != -1 && !bm_is_valid(n_tex))
return texture;
//Set the new texture
texture = n_tex;
//If it is intentionally invalid, blank everything else
if(n_tex == -1)
{
num_frames = 0;
total_time = 1.0f;
}
else
{
//Determine the num_frames and total_time values.
int fps = 0;
this->num_frames = 1;
bm_get_info(texture, NULL, NULL, NULL, &this->num_frames, &fps);
this->total_time = (num_frames / ((fps > 0) ? (float)fps : 1.0f));
}
return texture;
}
//********************-----CLASS: texture_map-----********************//
int texture_map::FindTexture(int bm_handle)
{
if(!bm_is_valid(bm_handle))
return -1;
for(int i = 0; i < TM_NUM_TYPES; i++)
{
if (this->textures[i].GetTexture() == bm_handle)
return i;
}
return -1;
}
int texture_map::FindTexture(const char* fname)
{
if(fname == NULL || !strlen(fname))
return -1;
char buf[NAME_LENGTH];
for(int i = 0; i < TM_NUM_TYPES; i++)
{
bm_get_filename(this->textures[i].GetTexture(), buf);
if (!strextcmp(buf, fname)) {
return i;
}
}
return -1;
}
void texture_map::PageIn()
{
for(int i = 0; i < TM_NUM_TYPES; i++)
this->textures[i].PageIn();
}
void texture_map::PageOut(bool release)
{
for(int i = 0; i < TM_NUM_TYPES; i++)
this->textures[i].PageOut(release);
}
void texture_map::Clear()
{
is_ambient = false;
is_transparent = false;
for(int i = 0; i < TM_NUM_TYPES; i++)
this->textures[i].clear();
}
void texture_map::ResetToOriginal()
{
for(int i = 0; i < TM_NUM_TYPES; i++)
this->textures[i].ResetTexture();
}
bsp_polygon_data::bsp_polygon_data(ubyte* _bsp_data, int _bsp_data_size)
{
Polygon_vertices.clear();
Polygons.clear();
for (int i = 0; i < MAX_MODEL_TEXTURES; ++i) {
Num_verts[i] = 0;
Num_polies[i] = 0;
}
Num_flat_verts = 0;
Num_flat_polies = 0;
bsp_data_size = _bsp_data_size;
Macro_ubyte_bounds = _bsp_data + _bsp_data_size;
process_bsp(0, _bsp_data);
Macro_ubyte_bounds = nullptr;
}
void bsp_polygon_data::process_bsp(int offset, ubyte* bsp_data)
{
int id = w(bsp_data + offset);
int size = w(bsp_data + offset + 4);
bool end = id == OP_EOF;
while (!end) {
switch (id)
{
case OP_DEFPOINTS:
process_defpoints(offset, bsp_data);
break;
case OP_SORTNORM:
process_sortnorm(offset, bsp_data);
break;
case OP_SORTNORM2:
process_sortnorm2(offset, bsp_data);
end = true; // should not continue after this chunk
break;
case OP_FLATPOLY:
process_flat(offset, bsp_data);
break;
case OP_TMAPPOLY:
process_tmap(offset, bsp_data);
break;
case OP_BOUNDBOX:
break;
case OP_TMAP2POLY:
process_tmap2(offset, bsp_data);
end = true; // should not continue after this chunk
break;
default:
return;
}
if (end) break;
offset += size;
id = w(bsp_data + offset);
size = w(bsp_data + offset + 4);
if (size < 1 || id == OP_EOF)
end = true;
}
}
void bsp_polygon_data::process_defpoints(int off, ubyte* bsp_data)
{
uint i, n;
uint nverts = uw(off + bsp_data + 8);
uint offset = uw(off + bsp_data + 16);
ubyte *normcount = off + bsp_data + 20;
vec3d *src = vp(off + bsp_data + offset);
// Get pointer to lights
Lights = off + bsp_data + 20 + nverts;
#ifndef NDEBUG
modelstats_num_verts += nverts;
#endif
Vertex_list.clear();
Normal_list.clear();
for (n = 0; n < nverts; n++) {
Vertex_list.push_back(*src);
src++; // move to normal
for (i = 0; i < normcount[n]; i++) {
Normal_list.push_back(*src);
src++;
}
}
}
/**
* @brief Parses a SORTNORM2 by recursively parsing into the two pointers it contains.
*
* @param offset The byte offset to the current SORT2NORM chunk within bsp_data.
* @param bsp_data The byte buffer containing the BSP information for the current model.
*/
void bsp_polygon_data::process_sortnorm2(int offset, ubyte* bsp_data)
{
int frontlist, backlist;
frontlist = w(bsp_data + offset + 8);
backlist = w(bsp_data + offset + 12);
if (backlist) process_bsp(offset + backlist, bsp_data);
if (frontlist) process_bsp(offset + frontlist, bsp_data);
}
void bsp_polygon_data::process_sortnorm(int offset, ubyte* bsp_data)
{
int frontlist, backlist, prelist, postlist, onlist;
frontlist = w(bsp_data + offset + 36);
backlist = w(bsp_data + offset + 40);
prelist = w(bsp_data + offset + 44);
postlist = w(bsp_data + offset + 48);
onlist = w(bsp_data + offset + 52);
if (prelist) process_bsp(offset + prelist, bsp_data);
if (backlist) process_bsp(offset + backlist, bsp_data);
if (onlist) process_bsp(offset + onlist, bsp_data);
if (frontlist) process_bsp(offset + frontlist, bsp_data);
if (postlist) process_bsp(offset + postlist, bsp_data);
}
void bsp_polygon_data::process_tmap(int offset, ubyte* bsp_data)
{
int pof_tex = w(bsp_data + offset + TMAP_TEXNUM);
uint n_vert = uw(bsp_data + offset + TMAP_NVERTS);
ubyte* p;
if ( n_vert < 3 ) {
// don't parse degenerate polygons
return;
}
p = &bsp_data[offset + TMAP_NORMAL];
auto tverts = reinterpret_cast<model_tmap_vert_old*>(&bsp_data[offset + TMAP_VERTS]);
// Copy the verts manually since they aren't aligned with the struct
//unpack_tmap_verts(&bsp_data[offset + 44], tverts, n_vert);
int problem_count = 0;
// make a polygon
bsp_polygon polygon;
polygon.Start_index = (uint)Polygon_vertices.size();
polygon.Num_verts = n_vert;
polygon.texture = pof_tex;
// this polygon will be broken up into a triangle fan. first three verts make up the first triangle
// additional verts are made into new tris
Num_polies[pof_tex]++;
Num_verts[pof_tex] += n_vert;
// stuff data making up the vertices of this polygon
for ( uint i = 0; i < n_vert; ++i ) {
bsp_vertex vert;
vert.position = Vertex_list[tverts[i].vertnum];
vert.tex_coord.u = tverts[i].u;
vert.tex_coord.v = tverts[i].v;
vert.normal = Normal_list[tverts[i].normnum];
// see if this normal is okay
if (IS_VEC_NULL(&vert.normal))
vert.normal = *vp(p);
problem_count += check_values(&vert.normal);
vm_vec_normalize_safe(&vert.normal);
Polygon_vertices.push_back(vert);
}
Polygons.push_back(polygon);
Parse_normal_problem_count += problem_count;
}
/**
* @brief Converts a TMAP2POLY chunk into a list of BSP_polygon.
*
* @param offset The byte offset into bsp_data.
* @param[in] The buffer containing the chunk data.
*/
void bsp_polygon_data::process_tmap2(int offset, ubyte* bsp_data)
{
int pof_tex = w(bsp_data + offset + TMAP2_TEXNUM);
uint n_vert = uw(bsp_data + offset + TMAP2_NVERTS);
model_tmap_vert* tverts;
int problem_count = 0;
bsp_polygon polygon;
if (n_vert < 3) {
Error(LOCATION, "Model contains TMAP2 chunk with less than 3 vertices!");
return;
}
tverts = reinterpret_cast<model_tmap_vert*>(&bsp_data[offset + TMAP2_VERTS]);
// make a polygon
polygon.Start_index = (uint)Polygon_vertices.size();
polygon.Num_verts = n_vert;
polygon.texture = pof_tex;
// this polygon will be broken up into a triangle fan. first three verts make up the first triangle
// additional verts are made into new tris
Num_polies[pof_tex]++;
Num_verts[pof_tex] += n_vert;
// stuff data making up the vertices of this polygon
for (uint i = 0; i < n_vert; ++i) {
bsp_vertex vert;
vert.position = Vertex_list[tverts[i].vertnum];
vert.tex_coord.u = tverts[i].u;
vert.tex_coord.v = tverts[i].v;
vert.normal = Normal_list[tverts[i].normnum];
// see if this normal is okay
if (IS_VEC_NULL(&vert.normal))
vert.normal = *vp(&bsp_data[offset + 32]);
problem_count += check_values(&vert.normal);
vm_vec_normalize_safe(&vert.normal);
Polygon_vertices.push_back(vert);
}
Polygons.push_back(polygon);
Parse_normal_problem_count += problem_count;
}
void bsp_polygon_data::process_flat(int offset, ubyte* bsp_data)
{
int n_vert = w(bsp_data + offset + 36);
if (n_vert < 3) {
// don't parse degenerate polygons
return;
}
short * verts = (short *)(bsp_data + offset + 44);
ubyte r = *(bsp_data + offset + 40);
ubyte g = *(bsp_data + offset + 41);
ubyte b = *(bsp_data + offset + 42);
bsp_polygon polygon;
polygon.Start_index = (uint)Polygon_vertices.size();
polygon.Num_verts = n_vert;
polygon.texture = -1;
Num_flat_polies++;
Num_flat_verts += n_vert;
for (int i = 0; i < n_vert; i++) {
bsp_vertex vert;
int vertnum = verts[i * 2 + 0];
int norm = verts[i * 2 + 1];
vert.position = Vertex_list[vertnum];
vert.normal = Normal_list[norm];
vert.r = r;
vert.g = g;
vert.b = b;
vert.a = 255;
Polygon_vertices.push_back(vert);
}
Polygons.push_back(polygon);
}
int bsp_polygon_data::get_num_triangles(int texture)
{
if ( texture < 0 ) {
return MAX(Num_flat_verts - 2 * Num_flat_polies, 0);
}
return MAX(Num_verts[texture] - 2 * Num_polies[texture], 0);
}
int bsp_polygon_data::get_num_lines(int texture)
{
if (texture < 0) {
return Num_flat_verts;
}
return Num_verts[texture];
}
void bsp_polygon_data::generate_triangles(int texture, vertex *vert_ptr, vec3d* norm_ptr)
{
int num_verts = 0;
for ( uint i = 0; i < Polygons.size(); ++i ) {
if ( Polygons[i].texture != texture ) {
continue;
}
uint start_index = Polygons[i].Start_index;
uint end_index = Polygons[i].Start_index + Polygons[i].Num_verts;
for ( uint j = start_index + 1; j < end_index - 1; ++j ) {
// first vertex of this triangle. Always the first vertex of the polygon
vertex* vert = &vert_ptr[num_verts];
vert->world = Polygon_vertices[start_index].position;
vert->texture_position = Polygon_vertices[start_index].tex_coord;
vec3d* norm = &norm_ptr[num_verts];
*norm = Polygon_vertices[start_index].normal;
// second vertex of this triangle.
vert = &vert_ptr[num_verts + 1];
vert->world = Polygon_vertices[j].position;
vert->texture_position = Polygon_vertices[j].tex_coord;
norm = &norm_ptr[num_verts + 1];
*norm = Polygon_vertices[j].normal;
// third vertex of this triangle.
vert = &vert_ptr[num_verts + 2];
vert->world = Polygon_vertices[j+1].position;
vert->texture_position = Polygon_vertices[j+1].tex_coord;
norm = &norm_ptr[num_verts + 2];
*norm = Polygon_vertices[j+1].normal;
num_verts += 3;
}
}
}
void bsp_polygon_data::generate_lines(int texture, vertex *vert_ptr)
{
int num_verts = 0;
for (uint i = 0; i < Polygons.size(); ++i) {
if (Polygons[i].texture != texture) {
continue;
}
uint start_index = Polygons[i].Start_index;
uint end_index = Polygons[i].Start_index + Polygons[i].Num_verts;
for (uint j = start_index; j < end_index; ++j) {
// first vertex of this triangle. Always the first vertex of the polygon
vertex* vert = &vert_ptr[num_verts];
vert->world = Polygon_vertices[j].position;
vert->r = Polygon_vertices[j].r;
vert->g = Polygon_vertices[j].g;
vert->b = Polygon_vertices[j].b;
vert->a = Polygon_vertices[j].a;
if ( j == end_index - 1 ) {
vert = &vert_ptr[num_verts + 1];
vert->world = Polygon_vertices[start_index].position;
vert->r = Polygon_vertices[start_index].r;
vert->g = Polygon_vertices[start_index].g;
vert->b = Polygon_vertices[start_index].b;
vert->a = Polygon_vertices[start_index].a;
} else {
vert = &vert_ptr[num_verts + 1];
vert->world = Polygon_vertices[j + 1].position;
vert->r = Polygon_vertices[j + 1].r;
vert->g = Polygon_vertices[j + 1].g;
vert->b = Polygon_vertices[j + 1].b;
vert->a = Polygon_vertices[j + 1].a;
}
num_verts += 2;
}
}
}
SCP_set<int> bsp_polygon_data::get_textures_used() const {
SCP_set<int> textures;
for (const auto& poly : Polygons)
textures.emplace(poly.texture);
return textures;
}
void bsp_polygon_data::replace_textures_used(const SCP_map<int, int>& replacementMap) {
for (auto& poly : Polygons) {
auto it = replacementMap.find(poly.texture);
if (it != replacementMap.end()) {
poly.texture = it->second;
Num_verts[it->first] -= poly.Num_verts;
Num_verts[it->second] += poly.Num_verts;
--Num_polies[it->first];
++Num_polies[it->second];
}
}
}
SCP_set<int> model_get_textures_used(const polymodel* pm, int submodel) {
return bsp_polygon_data{ pm->submodel[submodel].bsp_data.get(), pm->submodel[submodel].bsp_data_size }.get_textures_used();
}