mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-17 13:00:25 +02:00
Grow the powerups' render spheres in a port file
The powerup bounds of #117 move from source/models/models.c to port/linux/game/powerup_render_bounds.c, called from scenario_tags_load beside the other tags-loaded hooks, so the game's models.c no longer takes <xtl.h> and D3D buffer locks. It uses square_root and a relative margin in place of sqrtf and nextafterf, which strict gnu89 does not declare, and the objects.h scale change is marked as the port's. tools/test_powerup_bounds.py is dropped: it redeclares the engine's types by hand and CI does not run it. On bloodgulch the active camouflage's render radius grows from 0.1 to 0.23 and the overshield's from 0.1 to 0.21; no other equipment changes.
This commit is contained in:
1 parent
ec23cfcf24
commit
65a6bb7edd
6 files changed
+198
-337
No files matched your search
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
POWERUP_RENDER_BOUNDS.C
|
||||
|
||||
The overshield's and active camouflage's authored render spheres (the
|
||||
object's render_bounding_radius, which only culling reads: objects.h,
|
||||
object_get_render_bounding_sphere) are smaller than their meshes, so near the
|
||||
edges of the screen they were culled while still in view. When a map's tags
|
||||
load (scenario_tags_load), each powerup of those two kinds with a rigid
|
||||
one-node model and no animations gets a radius that holds every vertex of
|
||||
every geometry and part of its model, around its bounding offset. The radius
|
||||
only ever grows; the bounding radius physics, pickups and the game state use
|
||||
is left as it is, so nothing played or sent between machines changes.
|
||||
*/
|
||||
|
||||
#include "cseries.h"
|
||||
#include "math/real_math.h"
|
||||
#include "cache/cache_files.h"
|
||||
#include "items/equipment_definitions.h"
|
||||
#include "models/model_definitions.h"
|
||||
#include "rasterizer/rasterizer.h"
|
||||
#include "rasterizer/rasterizer_geometry.h"
|
||||
#include "rasterizer/rasterizer_model_types.h"
|
||||
#include "tag_files/tag_groups.h"
|
||||
|
||||
#include <float.h>
|
||||
#include <xtl.h>
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
enum
|
||||
{
|
||||
_model_geometry_part_stripped_bit = 0,
|
||||
_model_geometry_part_local_nodes_bit,
|
||||
};
|
||||
|
||||
/* ---------- structures */
|
||||
|
||||
/* this build's model geometry and part, as models.c and rasterizer.c each
|
||||
define them for themselves (no header declares them) */
|
||||
struct model_geometry
|
||||
{
|
||||
byte reserved[0x24];
|
||||
struct tag_block parts;
|
||||
};
|
||||
|
||||
struct model_geometry_part
|
||||
{
|
||||
unsigned long flags;
|
||||
short shader_index;
|
||||
char previous_part_index;
|
||||
char next_part_index;
|
||||
short centroid_primary_node_index;
|
||||
short centroid_secondary_node_index;
|
||||
real centroid_primary_node_weight;
|
||||
real centroid_secondary_node_weight;
|
||||
real_point3d centroid;
|
||||
struct tag_block uncompressed_vertices;
|
||||
struct tag_block compressed_vertices;
|
||||
struct tag_block triangles;
|
||||
struct triangle_buffer triangle_buffer;
|
||||
struct vertex_buffer vertex_buffer;
|
||||
};
|
||||
|
||||
typedef char verify_model_geometry_part_size[sizeof(struct model_geometry_part) == 0x68 ? 1 : -1];
|
||||
|
||||
/* ---------- private code */
|
||||
|
||||
/* the radius around center that holds every vertex of a rigid model's
|
||||
geometries; FALSE if the model is not one, or a vertex cannot be read */
|
||||
static boolean model_rigid_render_radius(
|
||||
struct model const *model,
|
||||
real_point3d const *center,
|
||||
real *radius)
|
||||
{
|
||||
struct model_node const *root;
|
||||
real maximum_squared = 0.f;
|
||||
long vertex_count = 0;
|
||||
long geometry_index;
|
||||
|
||||
if (model->nodes.count != 1 || !model->nodes.address ||
|
||||
model->geometries.count <= 0 || model->geometries.count > MAXIMUM_GEOMETRIES_PER_MODEL ||
|
||||
!model->geometries.address)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* every level of detail and permutation, in the root node's space */
|
||||
root = TAG_BLOCK_GET_ELEMENT(&model->nodes, 0, struct model_node);
|
||||
for (geometry_index = 0; geometry_index < model->geometries.count; geometry_index++)
|
||||
{
|
||||
struct model_geometry const *geometry = TAG_BLOCK_GET_ELEMENT(&model->geometries, geometry_index, struct model_geometry);
|
||||
long part_index;
|
||||
|
||||
if (geometry->parts.count < 0 || geometry->parts.count > MAXIMUM_PARTS_PER_MODEL_GEOMETRY ||
|
||||
(geometry->parts.count && !geometry->parts.address))
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
for (part_index = 0; part_index < geometry->parts.count; part_index++)
|
||||
{
|
||||
struct model_geometry_part const *part = TAG_BLOCK_GET_ELEMENT(&geometry->parts, part_index, struct model_geometry_part);
|
||||
struct vertex_buffer const *buffer = &part->vertex_buffer;
|
||||
byte *vertices = NULL;
|
||||
boolean valid = TRUE;
|
||||
long stride;
|
||||
long vertex_index;
|
||||
|
||||
if (TEST_FLAG(part->flags, _model_geometry_part_stripped_bit))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (TEST_FLAG(part->flags, _model_geometry_part_local_nodes_bit) || !buffer->hardware_format || buffer->offset ||
|
||||
buffer->count <= 0 || buffer->count > MAXIMUM_VERTICES_PER_MODEL_GEOMETRY_PART ||
|
||||
(buffer->type != _rasterizer_vertex_type_model_compressed && buffer->type != _rasterizer_vertex_type_model_uncompressed))
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
stride = rasterizer_geometry_get_vertex_size(buffer->type);
|
||||
IDirect3DVertexBuffer8_Lock(buffer->hardware_format, 0, 0, &vertices, D3DLOCK_READONLY);
|
||||
valid = vertices != NULL;
|
||||
for (vertex_index = 0; valid && vertex_index < buffer->count; vertex_index++)
|
||||
{
|
||||
real_point3d point;
|
||||
real distance_squared;
|
||||
|
||||
matrix4x3_transform_point(&root->runtime_default_inverse_matrix,
|
||||
(real_point3d const *)(vertices + vertex_index * stride), &point);
|
||||
distance_squared = distance_squared3d(&point, center);
|
||||
/* (not a number, or infinite) */
|
||||
if (!(distance_squared >= 0.f && distance_squared < FLT_MAX))
|
||||
{
|
||||
valid = FALSE;
|
||||
}
|
||||
else
|
||||
{
|
||||
maximum_squared = MAX(maximum_squared, distance_squared);
|
||||
}
|
||||
}
|
||||
IDirect3DVertexBuffer8_Unlock(buffer->hardware_format);
|
||||
if (!valid)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
vertex_count += buffer->count;
|
||||
}
|
||||
}
|
||||
|
||||
if (!vertex_count)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* a little past the farthest vertex, so its rounding leaves it inside */
|
||||
*radius = square_root(maximum_squared) * 1.0001f;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
void powerup_render_bounds_tags_loaded(
|
||||
void)
|
||||
{
|
||||
struct tag_iterator iterator;
|
||||
long equipment_index;
|
||||
|
||||
tag_iterator_new(&iterator, EQUIPMENT_DEFINITION_TAG);
|
||||
while ((equipment_index = tag_iterator_next(&iterator)) != NONE)
|
||||
{
|
||||
struct equipment_definition *equipment = equipment_definition_get(equipment_index);
|
||||
real radius;
|
||||
|
||||
if ((equipment->equipment.powerup_type == _equipment_powerup_overshield ||
|
||||
equipment->equipment.powerup_type == _equipment_powerup_active_camouflage) &&
|
||||
equipment->object.model.index != NONE && equipment->object.animation_graph.index == NONE &&
|
||||
model_rigid_render_radius(model_definition_get(equipment->object.model.index), &equipment->object.bounding_offset, &radius) &&
|
||||
radius > equipment->object.render_bounding_radius)
|
||||
{
|
||||
equipment->object.render_bounding_radius = radius;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
Vendored
+7
-4
@@ -134,7 +134,6 @@ symbols in this file:
|
||||
#include "interface/ui_widget.h"
|
||||
#include "scenario/scenario_definitions.h"
|
||||
#include "sound/sound_manager.h"
|
||||
#include "models/models.h"
|
||||
#include "tag_schema.h"
|
||||
#include "custom_edition_cache.h" /* port: port/linux/game/custom_edition_cache.c */
|
||||
#include "cache_file_formats.h" /* port: CUSTOM_EDITION_TAG_CACHE_BYTES */
|
||||
@@ -1271,9 +1270,13 @@ long scenario_tags_load(
|
||||
|
||||
pal_tags_loaded(cache_file_globals.header.build);
|
||||
}
|
||||
/* The two powerups' authored render spheres can be smaller than
|
||||
* their rigid meshes. Derive their bounds while tags are writable. */
|
||||
models_fix_powerup_render_bounds();
|
||||
/* port: the powerups' render spheres, grown to hold their meshes
|
||||
(port/linux/game/powerup_render_bounds.c) */
|
||||
{
|
||||
extern void powerup_render_bounds_tags_loaded(void);
|
||||
|
||||
powerup_render_bounds_tags_loaded();
|
||||
}
|
||||
/* port: the menus' tags, added to the map's (port/linux/game/menu_tags.c) */
|
||||
{
|
||||
extern void menu_tags_loaded(char const *map_name);
|
||||
|
||||
@@ -83,10 +83,6 @@ symbols in this file:
|
||||
#include "shaders/shaders.h"
|
||||
#include "rasterizer/rasterizer_console_vars.h"
|
||||
#include "rasterizer/rasterizer_model_types.h"
|
||||
#include "cache/cache_files.h"
|
||||
#include "items/equipment_definitions.h"
|
||||
#include <float.h>
|
||||
#include <xtl.h>
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
@@ -462,82 +458,6 @@ static void render_model_parts(
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
static boolean model_rigid_render_radius(
|
||||
struct model const *model,
|
||||
real_point3d const *center,
|
||||
real *radius)
|
||||
{
|
||||
struct model_node const *root;
|
||||
real maximum_squared = 0.0f;
|
||||
long geometry_index, vertex_count = 0;
|
||||
if (model->nodes.count != 1 || !model->nodes.address ||
|
||||
model->geometries.count <= 0 || model->geometries.count > MAXIMUM_GEOMETRIES_PER_MODEL ||
|
||||
!model->geometries.address) return FALSE;
|
||||
root = TAG_BLOCK_GET_ELEMENT(&model->nodes, 0, struct model_node);
|
||||
/* Include every LOD/permutation. A one-node model has a fixed mesh in
|
||||
* root-node space; skeletal animation needs a different bounds policy. */
|
||||
for (geometry_index = 0; geometry_index < model->geometries.count; ++geometry_index)
|
||||
{
|
||||
struct model_geometry const *geometry = TAG_BLOCK_GET_ELEMENT(&model->geometries, geometry_index, struct model_geometry);
|
||||
long part_index;
|
||||
if (geometry->parts.count < 0 || geometry->parts.count > MAXIMUM_PARTS_PER_MODEL_GEOMETRY ||
|
||||
(geometry->parts.count && !geometry->parts.address)) return FALSE;
|
||||
for (part_index = 0; part_index < geometry->parts.count; ++part_index)
|
||||
{
|
||||
struct model_geometry_part const *part = TAG_BLOCK_GET_ELEMENT(&geometry->parts, part_index, struct model_geometry_part);
|
||||
struct vertex_buffer const *buffer = &part->vertex_buffer;
|
||||
byte *vertices = NULL;
|
||||
long stride, i;
|
||||
boolean valid = TRUE;
|
||||
if (TEST_FLAG(part->flags, _model_geometry_part_stripped_bit)) continue;
|
||||
if (TEST_FLAG(part->flags, _model_geometry_part_local_nodes_bit) || !buffer->hardware_format || buffer->offset ||
|
||||
buffer->count <= 0 || buffer->count > MAXIMUM_VERTICES_PER_MODEL_GEOMETRY_PART ||
|
||||
(buffer->type != _rasterizer_vertex_type_model_compressed && buffer->type != _rasterizer_vertex_type_model_uncompressed)) return FALSE;
|
||||
stride = rasterizer_geometry_get_vertex_size(buffer->type);
|
||||
IDirect3DVertexBuffer8_Lock(buffer->hardware_format, 0, 0, &vertices, D3DLOCK_READONLY);
|
||||
if (!vertices) valid = FALSE;
|
||||
for (i = 0; valid && i < buffer->count; ++i)
|
||||
{
|
||||
real_point3d point;
|
||||
real squared;
|
||||
matrix4x3_transform_point(&root->runtime_default_inverse_matrix,
|
||||
(real_point3d const *)(vertices + i * stride), &point);
|
||||
squared = distance_squared3d(&point, center);
|
||||
if (!(squared >= 0.0f && squared < FLT_MAX)) valid = FALSE;
|
||||
else maximum_squared = MAX(maximum_squared, squared);
|
||||
}
|
||||
IDirect3DVertexBuffer8_Unlock(buffer->hardware_format);
|
||||
if (!valid) return FALSE;
|
||||
vertex_count += buffer->count;
|
||||
}
|
||||
}
|
||||
if (!vertex_count) return FALSE;
|
||||
/* Round outward so the extremal vertex stays inside the sphere. */
|
||||
*radius = nextafterf(sqrtf(maximum_squared), FLT_MAX);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void models_fix_powerup_render_bounds(void)
|
||||
{
|
||||
struct tag_iterator iterator;
|
||||
long index;
|
||||
tag_iterator_new(&iterator, EQUIPMENT_DEFINITION_TAG);
|
||||
while ((index = tag_iterator_next(&iterator)) != NONE)
|
||||
{
|
||||
struct equipment_definition *equipment = equipment_definition_get(index);
|
||||
real radius;
|
||||
if ((equipment->equipment.powerup_type == _equipment_powerup_overshield ||
|
||||
equipment->equipment.powerup_type == _equipment_powerup_active_camouflage) &&
|
||||
equipment->object.model.index != NONE && equipment->object.animation_graph.index == NONE &&
|
||||
model_rigid_render_radius(model_definition_get(equipment->object.model.index), &equipment->object.bounding_offset, &radius) &&
|
||||
radius > equipment->object.render_bounding_radius)
|
||||
{
|
||||
/* Derived tag data only: leave physics, pickup radius and saves intact. */
|
||||
equipment->object.render_bounding_radius = radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void model_interpolate_node_orientations(
|
||||
struct model const *model,
|
||||
struct real_orientation *original_node_orientations,
|
||||
|
||||
@@ -27,8 +27,6 @@ union real_vector3d;
|
||||
|
||||
/* ---------- prototypes/MODELS.C */
|
||||
|
||||
void models_fix_powerup_render_bounds(void);
|
||||
|
||||
void model_interpolate_node_orientations(
|
||||
struct model const *model,
|
||||
struct real_orientation *original_node_orientations,
|
||||
|
||||
@@ -529,10 +529,12 @@ __inline void object_get_render_bounding_sphere(
|
||||
|
||||
*center = object->object.bounding_sphere_center;
|
||||
*radius = object_definition_get(object->definition_index)->object.render_bounding_radius;
|
||||
/* Growing an object must grow its culling sphere. Preserve the existing
|
||||
* conservative sphere for scales at or below one. */
|
||||
if (object->object.scale > 1.0f)
|
||||
/* port: (grown with an object scaled up, as its bounding sphere is in
|
||||
object_compute_node_matrices; one scaled down keeps the larger sphere) */
|
||||
if (object->object.scale > 1.f)
|
||||
{
|
||||
*radius *= object->object.scale;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
"""Test production powerup bounds and frustum culling, without a game/profile.
|
||||
|
||||
--map additionally measures both actual Xbox powerup meshes. Assets stay local.
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
import os
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zlib
|
||||
ROOT=Path(__file__).resolve().parent.parent
|
||||
|
||||
def block(source, marker):
|
||||
start = source.index(marker)
|
||||
end = source.index('{', start) + 1
|
||||
depth = 1
|
||||
while depth:
|
||||
depth += (source[end] == '{') - (source[end] == '}')
|
||||
end += 1
|
||||
return source[start:end]
|
||||
|
||||
PRELUDE=r'''
|
||||
#define _CRT_SECURE_NO_WARNINGS
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <float.h>
|
||||
typedef unsigned char byte,boolean;
|
||||
typedef unsigned short word;
|
||||
typedef float real;
|
||||
typedef union {struct {real x,y,z;};struct {real i,j,k;};real n[3];} real_point3d,real_vector3d;
|
||||
typedef struct {real i,j;} real_vector2d;
|
||||
typedef struct {real red,green,blue;} real_rgb_color;
|
||||
typedef struct {real i,j,k,w;} real_quaternion;
|
||||
typedef struct real_matrix4x3 {real scale;real_vector3d forward,left,up;real_point3d position;} real_matrix4x3;
|
||||
typedef struct {real_vector3d n;real d;} real_plane3d;
|
||||
typedef struct {real x0,x1,y0,y1,z0,z1;} real_rectangle3d;
|
||||
struct render_frustum {real_rectangle3d world_bounds;real_plane3d world_planes[6];};
|
||||
struct tag_block {long count;void *address,*definition;};
|
||||
struct tag_reference {unsigned long group_tag;char *name;long name_length,index;};
|
||||
struct tag_iterator {long absolute_index;};
|
||||
#define TRUE 1
|
||||
#define FALSE 0
|
||||
#define NONE -1
|
||||
#define FLAG(b) (1u<<(b))
|
||||
#define TEST_FLAG(f,b) (((f)&FLAG(b))!=0)
|
||||
#define MAX(a,b) ((a)>(b)?(a):(b))
|
||||
#define TAG_STRING_LENGTH 31
|
||||
#define UNSIGNED_SHORT_MAX 65535
|
||||
#define TAG_BLOCK_GET_ELEMENT(b,i,t) ((t*)((byte*)(b)->address+(i)*sizeof(t)))
|
||||
#define D3DLOCK_READONLY 128
|
||||
#define CHECK(c) do{if(!(c)){fprintf(stderr,"FAIL %d: %s\n",__LINE__,#c);exit(1);}}while(0)
|
||||
#define match_assert(file,line,c) CHECK(c)
|
||||
#define OBJECTS_H_FILE "test"
|
||||
struct test_buffer {byte *data;};
|
||||
static void IDirect3DVertexBuffer8_Lock(void *b,int offset,int size,byte **data,int flags){*data=b&&((struct test_buffer*)b)->data?((struct test_buffer*)b)->data+offset:NULL;}
|
||||
static void IDirect3DVertexBuffer8_Unlock(void *b){}
|
||||
static long rasterizer_geometry_get_vertex_size(short type){return type==5?32:68;}
|
||||
'''
|
||||
HARNESS=r'''
|
||||
static struct model models[8];
|
||||
static struct equipment_definition equipment[8];
|
||||
static long equipment_count;
|
||||
#define equipment_definition_get(i) (equipment+(i))
|
||||
#define object_definition_get(i) ((struct object_definition*)(equipment+(i)))
|
||||
#define model_definition_get(i) (models+(i))
|
||||
static void tag_iterator_new(struct tag_iterator *i,long group){i->absolute_index=0;}
|
||||
static long tag_iterator_next(struct tag_iterator *i){return i->absolute_index<equipment_count?i->absolute_index++:NONE;}
|
||||
struct object_datum {long definition_index;struct {real_point3d bounding_sphere_center;real scale;} object;};
|
||||
static struct object_datum object;
|
||||
static struct object_datum *object_get(long i){return &object;}
|
||||
'''
|
||||
TESTS=r'''
|
||||
static struct model_node node;
|
||||
static struct model_geometry geometries[2];
|
||||
static struct model_geometry_part parts[2];
|
||||
static float vertices[2][3][17];
|
||||
static struct test_buffer buffers[2];
|
||||
static void initialize(void){
|
||||
memset(equipment,0,sizeof(equipment));memset(models,0,sizeof(models));memset(&node,0,sizeof(node));
|
||||
memset(geometries,0,sizeof(geometries));memset(parts,0,sizeof(parts));memset(vertices,0,sizeof(vertices));
|
||||
node.runtime_default_inverse_matrix=(real_matrix4x3){.scale=1,.forward={.x=1},.left={.y=1},.up={.z=1},.position={.z=-.1f}};
|
||||
for(int i=0;i<2;i++){
|
||||
vertices[i][0][0]=.15f+i*.05f;vertices[i][0][2]=.15f;vertices[i][1][1]=-.15f;vertices[i][2][2]=.3f+i*.1f;
|
||||
buffers[i].data=(byte*)vertices[i];parts[i].vertex_buffer=(struct vertex_buffer){4,0,3,0,NULL,buffers+i};
|
||||
geometries[i].parts=(struct tag_block){1,parts+i,NULL};
|
||||
}
|
||||
models[0].nodes=(struct tag_block){1,&node,NULL};models[0].geometries=(struct tag_block){2,geometries,NULL};
|
||||
equipment_count=3;
|
||||
for(int i=0;i<3;i++){
|
||||
equipment[i].object.bounding_radius=.1f;equipment[i].object.render_bounding_radius=i==2?10:.1f;
|
||||
equipment[i].object.animation_graph.index=NONE;equipment[i].object.model.index=0;
|
||||
equipment[i].equipment.powerup_type=i==1?_equipment_powerup_health:_equipment_powerup_overshield;
|
||||
}
|
||||
}
|
||||
static void tests(void){
|
||||
initialize();struct equipment_definition before[3];memcpy(before,equipment,sizeof(before));
|
||||
models_fix_powerup_render_bounds();CHECK(equipment[0].object.render_bounding_radius>.3f);
|
||||
before[0].object.render_bounding_radius=equipment[0].object.render_bounding_radius;CHECK(!memcmp(before,equipment,sizeof(before)));
|
||||
real grown=equipment[0].object.render_bounding_radius;models_fix_powerup_render_bounds();CHECK(grown==equipment[0].object.render_bounding_radius);
|
||||
/* Atomic fallback: a valid first LOD must not mask a bad later LOD. */
|
||||
for(int c=0;c<14;c++){
|
||||
initialize();switch(c){case 0:models[0].nodes.count=2;break;case 1:parts[1].vertex_buffer.type=0;break;
|
||||
case 2:parts[1].vertex_buffer.hardware_format=NULL;break;case 3:parts[1].vertex_buffer.count=0;break;
|
||||
case 4:parts[1].flags=2;break;case 5:vertices[1][0][0]=NAN;break;case 6:models[0].geometries.address=NULL;break;
|
||||
case 7:equipment[0].object.model.index=NONE;equipment[2].object.model.index=NONE;break;
|
||||
case 8:equipment[0].object.animation_graph.index=5;equipment[2].object.animation_graph.index=5;break;
|
||||
case 9:parts[1].vertex_buffer.offset=1;break;case 10:buffers[1].data=NULL;break;case 11:models[0].nodes.address=NULL;break;case 12:node.runtime_default_inverse_matrix.scale=NAN;break;case 13:models[0].geometries.count=257;break;}
|
||||
memcpy(before,equipment,sizeof(before));models_fix_powerup_render_bounds();CHECK(!memcmp(before,equipment,sizeof(before)));
|
||||
}
|
||||
initialize();equipment[0].object.bounding_offset=(real_point3d){.x=.3f,.z=.1f};
|
||||
real radius;CHECK(model_rigid_render_radius(models,&equipment[0].object.bounding_offset,&radius));CHECK(radius>.39f);
|
||||
parts[1].flags=1;CHECK(model_rigid_render_radius(models,&equipment[0].object.bounding_offset,&radius));
|
||||
/* compressed and uncompressed vertices have identical positions */
|
||||
float packed[3][8]={0};for(int i=0;i<3;i++)memcpy(packed[i],vertices[0][i],12);
|
||||
parts[0].vertex_buffer.type=5;buffers[0].data=(byte*)packed;
|
||||
CHECK(model_rigid_render_radius(models,&equipment[0].object.bounding_offset,&radius));
|
||||
object.definition_index=0;object.object.bounding_sphere_center=(real_point3d){.x=3,.y=4,.z=5};
|
||||
float scales[]={0,.1f,.5f,1,2,4};
|
||||
for(int i=0;i<6;i++){real_point3d center;object.object.scale=scales[i];object_get_render_bounding_sphere(0,¢er,&radius);
|
||||
CHECK(!memcmp(¢er,&object.object.bounding_sphere_center,sizeof(center)));
|
||||
CHECK(radius==equipment[0].object.render_bounding_radius*(scales[i]>1?scales[i]:1));}
|
||||
puts("PASS: all LODs, root transform/offset, both vertex formats, conservative/idempotent growth, 14 atomic fallback paths, scaled culling; only render radius changes");
|
||||
}
|
||||
static void frustum(struct render_frustum *f,float fov,float aspect){
|
||||
float tv=tanf(fov*.5f),th=tv*aspect,h=1/sqrtf(1+th*th),v=1/sqrtf(1+tv*tv);
|
||||
f->world_bounds=(real_rectangle3d){-100*th,100*th,-100*tv,100*tv,-100,0};
|
||||
f->world_planes[0]=(real_plane3d){{.x=-h,.z=th*h},0};f->world_planes[1]=(real_plane3d){{.x=h,.z=th*h},0};
|
||||
f->world_planes[2]=(real_plane3d){{.y=-v,.z=tv*v},0};f->world_planes[3]=(real_plane3d){{.y=v,.z=tv*v},0};
|
||||
f->world_planes[4]=(real_plane3d){{.z=1},-.05f};f->world_planes[5]=(real_plane3d){{.z=-1},100};
|
||||
}
|
||||
static void sweeps(struct model *model,struct equipment_definition *definition){
|
||||
real local_radius=definition->object.render_bounding_radius;int checks=0,legacy_failures=0,culled=0;
|
||||
for(int size=0;size<4;size++)for(int fov=0;fov<3;fov++)for(int viewport=0;viewport<4;viewport++)for(int rotation=0;rotation<4;rotation++)for(int edge=0;edge<4;edge++)for(int step=-20;step<=20;step++){
|
||||
float scales[]={.25f,1,2,4},fovs[]={.6f,1.2f,1.8f},aspects[]={16.f/9,4.f/3,32.f/9,8.f/9};
|
||||
float scale=scales[size],depth=3,angle=rotation*.6f,tv=tanf(fovs[fov]*.5f),th=tv*aspects[viewport];
|
||||
real_matrix4x3 world={.scale=scale,.forward={.x=cosf(angle),.z=sinf(angle)},.left={.y=1},.up={.x=-sinf(angle),.z=cosf(angle)},.position={.z=-depth}};
|
||||
if(edge<2)world.position.x=(edge?1:-1)*(depth*th+step*.1f*local_radius*scale*sqrtf(1+th*th));
|
||||
else world.position.y=(edge==3?1:-1)*(depth*tv+step*.1f*local_radius*scale*sqrtf(1+tv*tv));
|
||||
struct render_frustum f;frustum(&f,fovs[fov],aspects[viewport]);
|
||||
real_point3d center;matrix4x3_transform_point(&world,&definition->object.bounding_offset,¢er);
|
||||
object.definition_index=0;equipment[0]=*definition;object.object.scale=scale;object.object.bounding_sphere_center=center;
|
||||
real radius;real_point3d queried;object_get_render_bounding_sphere(0,&queried,&radius);
|
||||
int visible=render_frustum_sphere_visible(&f,&queried,radius),has_vertex=0;
|
||||
for(int g=0;g<model->geometries.count;g++){
|
||||
struct model_geometry *geo=TAG_BLOCK_GET_ELEMENT(&model->geometries,g,struct model_geometry);
|
||||
for(int p=0;p<geo->parts.count;p++){
|
||||
struct vertex_buffer *buffer=&TAG_BLOCK_GET_ELEMENT(&geo->parts,p,struct model_geometry_part)->vertex_buffer;
|
||||
struct test_buffer *b=buffer->hardware_format;
|
||||
for(int i=0;i<buffer->count;i++){
|
||||
real_point3d root,point;matrix4x3_transform_point(&((struct model_node*)model->nodes.address)->runtime_default_inverse_matrix,(real_point3d*)(b->data+i*rasterizer_geometry_get_vertex_size(buffer->type)),&root);
|
||||
matrix4x3_transform_point(&world,&root,&point);CHECK(distance_squared3d(&point,¢er)<=radius*radius*1.00001f);
|
||||
int inside=1;for(int plane=0;plane<6;plane++)inside&=plane3d_distance_to_point(&f.world_planes[plane],&point)<-.000001f;
|
||||
has_vertex|=inside;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(has_vertex){CHECK(visible);legacy_failures+=!render_frustum_sphere_visible(&f,¢er,.1f);}
|
||||
culled+=!visible;checks++;
|
||||
}
|
||||
CHECK(legacy_failures>0&&culled>0);printf("PASS: %d edge sweeps; %d premature legacy culls; mesh containment, four viewport ratios, rotations, FOVs and scales; fully offscreen culling retained\n",checks,legacy_failures);
|
||||
}
|
||||
static void asset_test(const char *path){
|
||||
FILE *f=fopen(path,"rb");CHECK(f);unsigned int count;CHECK(fread(&count,4,1,f)==1&&count==2);
|
||||
for(int entry=0;entry<count;entry++){
|
||||
struct equipment_definition definition;CHECK(sizeof(definition)==800);CHECK(fread(&definition,sizeof(definition),1,f)==1);
|
||||
struct model_node root={0};CHECK(fread(&root.runtime_default_inverse_matrix,52,1,f)==1);
|
||||
struct model model={0};model.nodes=(struct tag_block){1,&root,NULL};unsigned int n;CHECK(fread(&n,4,1,f)==1);
|
||||
struct model_geometry geometry={0};struct model_geometry_part *parts=calloc(n,sizeof(*parts));struct test_buffer *buffers=calloc(n,sizeof(*buffers));
|
||||
geometry.parts=(struct tag_block){n,parts,NULL};model.geometries=(struct tag_block){1,&geometry,NULL};
|
||||
for(int i=0;i<n;i++){
|
||||
unsigned int h[3];CHECK(fread(h,4,3,f)==3);parts[i].flags=h[0];parts[i].vertex_buffer=(struct vertex_buffer){h[1],0,h[2],0,NULL,buffers+i};
|
||||
int stride=rasterizer_geometry_get_vertex_size(h[1]);buffers[i].data=malloc(stride*h[2]);CHECK(fread(buffers[i].data,stride,h[2],f)==h[2]);
|
||||
}
|
||||
real radius;CHECK(model_rigid_render_radius(&model,&definition.object.bounding_offset,&radius));
|
||||
equipment_count=1;equipment[0]=definition;models[0]=model;equipment[0].object.model.index=0;
|
||||
struct equipment_definition before=equipment[0];models_fix_powerup_render_bounds();CHECK(equipment[0].object.render_bounding_radius>.1f);
|
||||
before.object.render_bounding_radius=equipment[0].object.render_bounding_radius;CHECK(!memcmp(&before,equipment,sizeof(before)));
|
||||
definition.object.render_bounding_radius=equipment[0].object.render_bounding_radius;
|
||||
printf("Actual powerup type %d: radius %.6f -> %.6f\n",definition.equipment.powerup_type,.1f,radius);sweeps(&model,&definition);
|
||||
for(int i=0;i<n;i++)free(buffers[i].data);free(parts);free(buffers);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
int main(int argc,char **argv){tests();initialize();models_fix_powerup_render_bounds();struct equipment_definition def=equipment[0];sweeps(models,&def);if(argc>1)asset_test(argv[1]);return 0;}
|
||||
'''
|
||||
|
||||
def fixture(path):
|
||||
raw=path.read_bytes();data=raw if struct.unpack_from('<I',raw,8)[0]==len(raw) else raw[:2048]+zlib.decompressobj().decompress(raw[2048:])
|
||||
u=lambda p:struct.unpack_from('<I',data,p)[0]
|
||||
offset=u(16);base=u(offset)-36;off=lambda p:p-base+offset
|
||||
tags={};powerups=[]
|
||||
for i in range(u(offset+12)):
|
||||
o=offset+36+32*i;tags[u(o+12)]=off(u(o+20))
|
||||
if data[o:o+4]==b'piqe' and struct.unpack_from('<h',data,off(u(o+20))+776)[0] in (2,3):powerups.append(off(u(o+20)))
|
||||
assert len(powerups)==2
|
||||
result=struct.pack('<I',len(powerups))
|
||||
for o in powerups:
|
||||
model=tags[u(o+52)];assert u(model+184)==1
|
||||
node=off(u(model+188));result+=data[o:o+800]+data[node+104:node+156]
|
||||
parts=[]
|
||||
for g in range(u(model+208)):
|
||||
geo=off(u(model+212))+48*g
|
||||
parts.extend(off(u(geo+40))+104*p for p in range(u(geo+36)))
|
||||
result+=struct.pack('<I',len(parts))
|
||||
for p in parts:
|
||||
typ=struct.unpack_from('<h',data,p+84)[0];assert typ in (4,5)
|
||||
n=u(p+88);start=off(u(off(u(p+100))+4));stride=32 if typ==5 else 68
|
||||
result+=struct.pack('<III',u(p),typ,n)+data[start:start+stride*n]
|
||||
return result
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--cc',default='clang');parser.add_argument('--map',type=Path);args=parser.parse_args()
|
||||
read=lambda p:(ROOT/p).read_text()
|
||||
model_source=read('source/models/models.c');mh=read('source/models/model_definitions.h');rh=read('source/rasterizer/rasterizer_geometry.h')
|
||||
harness=re.sub(r'(?<!\.)\bequipment\b','test_equipment',HARNESS)
|
||||
tests=re.sub(r'(?<!\.)\bequipment\b','test_equipment',TESTS)
|
||||
unit=PRELUDE+block(mh,'enum\n{\n\tMODELS_GROUP_TAG')+';\n'+block(model_source,'enum\n{\n\t_model_geometry_part_stripped_bit')+';\n'+block(rh,'enum')+';\n'
|
||||
for src,names in [(rh,['vertex_buffer','triangle_buffer']), (mh,['model_node','model']),
|
||||
(model_source,['model_geometry','model_geometry_part']),
|
||||
(read('source/objects/object_definitions.h'),['_object_definition','object_definition']),
|
||||
(read('source/items/item_definitions.h'),['_item_definition']),
|
||||
(read('source/items/equipment_definitions.h'),['_equipment_definition','equipment_definition'])]:
|
||||
for name in names:unit+=block(src,'struct '+name+'\n')+';\n'
|
||||
unit+=block(read('source/items/equipment_definitions.h'),'enum equipment_powerup_type')+';\n#define EQUIPMENT_DEFINITION_TAG 0\n'+harness
|
||||
math=read('source/math/real_math.h')
|
||||
for name in ['vector_from_points3d','magnitude_squared3d','distance_squared3d','dot_product3d','plane3d_distance_to_point']:
|
||||
marker=('__inline real_vector3d *' if name=='vector_from_points3d' else '__inline real ')+name+'('
|
||||
unit+=block(math,marker).replace('__inline','static inline')+'\n'
|
||||
unit+=block(read('source/math/matrix_math.c'),'real_point3d *matrix4x3_transform_point(')+'\n'
|
||||
unit+=block(model_source,'static boolean model_rigid_render_radius(')+'\n'+block(model_source,'void models_fix_powerup_render_bounds(')+'\n'
|
||||
unit+=block(read('source/objects/objects.h'),'__inline void object_get_render_bounding_sphere(').replace('__inline','static')+'\n'
|
||||
unit+=block(read('source/render/render_cameras.c'),'short render_frustum_sphere_visible(')+'\n'+tests
|
||||
compiler=[args.cc,'-std=gnu11','-O2','-fuse-ld=lld']+(['--target=i686-pc-windows-msvc'] if sys.platform=='win32' else ['-m32','-lm'])
|
||||
with tempfile.TemporaryDirectory(prefix='halo-powerup-bounds-') as directory:
|
||||
path=Path(directory);(path/'test.c').write_text(unit);command=[str(path/'test.exe')]
|
||||
if args.map:(path/'asset.bin').write_bytes(fixture(args.map));command.append(str(path/'asset.bin'))
|
||||
subprocess.run([*compiler,str(path/'test.c'),'-o',str(path/'test.exe')],check=True)
|
||||
result=subprocess.run(command,check=True,timeout=45,capture_output=True,text=True)
|
||||
print(result.stdout,end='')
|
||||
assert 'PASS: all LODs' in result.stdout and 'edge sweeps' in result.stdout, result.stderr
|
||||
|
||||
if __name__=='__main__':main()
|
||||
Reference in new issue
Block a user