rt denoiser: motion vectors for raytracing pipeline and for ray query

This commit is contained in:
LifeKILLED 2023-01-23 04:00:36 +04:00
commit c4881f57c9
17 changed files with 168 additions and 13 deletions

View File

@ -17,6 +17,9 @@ layout(set = 0, binding = 4, rgba16f) uniform readonly image2D light_point_diffu
layout(set = 0, binding = 5, rgba16f) uniform readonly image2D light_point_specular;
layout(set = 0, binding = 6, rgba16f) uniform readonly image2D emissive;
layout(set = 0, binding = 7, rgba32f) uniform readonly image2D position_t;
layout(set = 0, binding = 8, rgba32f) uniform readonly image2D prev_position_t;
//layout(set = 0, binding = 7, rgba32f) uniform readonly image2D position_t;
//layout(set = 0, binding = 8, rgba16f) uniform readonly image2D normals_gs;
@ -204,6 +207,9 @@ void main() {
}
#endif
// DEBUG motion vectors
//colour = vec3(length(imageLoad(position_t, pix).rgb - imageLoad(prev_position_t, pix).rgb));
imageStore(out_dest, pix, vec4(colour, 0.));
//imageStore(out_dest, pix, imageLoad(light_poly, pix));
}

View File

@ -7,6 +7,7 @@
struct RayPayloadOpaque {
float t_offset, pixel_cone_spread_angle;
vec4 hit_pos_t;
vec4 prev_pos_t;
vec3 normal;
vec3 geometry_normal;
vec3 base_color;

View File

@ -18,6 +18,7 @@
X(12, normals_gs, rgba16f) \
X(13, material_rmxx, rgba8) \
X(14, emissive, rgba16f) \
X(15, prev_position_t, rgba32f) \
#define RAY_LIGHT_DIRECT_INPUTS(X) \
X(10, position_t, rgba32f) \
@ -104,6 +105,8 @@ struct Kusok {
float roughness;
float metalness;
PAD(2)
mat4 prev_transform;
};
struct PointLight {
@ -154,6 +157,7 @@ struct PushConstants {
struct UniformBuffer {
mat4 inv_proj, inv_view;
mat4 prev_inv_proj, prev_inv_view;
float ray_cone_width;
uint random_seed;
PAD(2)

View File

@ -9,6 +9,7 @@
struct Vertex {
vec3 pos;
vec3 prev_pos;
vec3 normal;
vec3 tangent;
vec2 gl_tc;

View File

@ -55,4 +55,5 @@ void main() {
imageStore(out_normals_gs, pix, payload.normals_gs);
imageStore(out_material_rmxx, pix, payload.material_rmxx);
imageStore(out_emissive, pix, payload.emissive);
imageStore(out_prev_position_t, pix, payload.prev_pos_t);
}

View File

@ -25,6 +25,7 @@ void main() {
Geometry geom = readHitGeometry(bary, ubo.ubo.ray_cone_width);
payload.hit_t = vec4(geom.pos, gl_HitTEXT);
payload.prev_pos_t = vec4(geom.prev_pos, 0.);
const Kusok kusok = getKusok(geom.kusok_index);
const uint tex_base_color = kusok.tex_base_color;

View File

@ -41,4 +41,5 @@ void main() {
imageStore(out_normals_gs, ivec2(gl_LaunchIDEXT.xy), payload.normals_gs);
imageStore(out_material_rmxx, ivec2(gl_LaunchIDEXT.xy), payload.material_rmxx);
imageStore(out_emissive, ivec2(gl_LaunchIDEXT.xy), payload.emissive);
imageStore(out_prev_position_t, ivec2(gl_LaunchIDEXT.xy), payload.prev_pos_t);
}

View File

@ -7,6 +7,7 @@
struct RayPayloadPrimary {
vec4 hit_t;
vec4 prev_pos_t;
vec4 base_color_a;
vec4 normals_gs;
vec4 material_rmxx;

View File

@ -21,6 +21,7 @@ void primaryRayHit(rayQueryEXT rq, inout RayPayloadPrimary payload) {
const float hitT = rayQueryGetIntersectionTEXT(rq, true); //gl_HitTEXT;
const vec3 rayDirection = rayQueryGetWorldRayDirectionEXT(rq); //gl_WorldRayDirectionEXT
payload.hit_t = vec4(geom.pos, hitT);
payload.prev_pos_t = vec4(geom.prev_pos, 0.);
const Kusok kusok = getKusok(geom.kusok_index);
const uint tex_base_color = kusok.tex_base_color;

View File

@ -42,6 +42,7 @@ vec4 computeAnisotropicEllipseAxes(in vec3 P, in vec3 f,
struct Geometry {
vec3 pos;
vec3 prev_pos;
vec2 uv;
vec4 uv_lods;
@ -87,6 +88,12 @@ Geometry readHitGeometry(vec2 bary, float ray_cone_width) {
objectToWorld * vec4(getVertex(vi3).pos, 1.f),
};
const vec3 prev_pos[3] = {
(kusok.prev_transform * vec4(getVertex(vi1).prev_pos, 1.f)).xyz,
(kusok.prev_transform * vec4(getVertex(vi2).prev_pos, 1.f)).xyz,
(kusok.prev_transform * vec4(getVertex(vi3).prev_pos, 1.f)).xyz,
};
const vec2 uvs[3] = {
getVertex(vi1).gl_tc,
getVertex(vi2).gl_tc,
@ -94,6 +101,7 @@ Geometry readHitGeometry(vec2 bary, float ray_cone_width) {
};
geom.pos = baryMix(pos[0], pos[1], pos[2], bary);
geom.prev_pos = baryMix(prev_pos[0], prev_pos[1], prev_pos[2], bary);
geom.uv = baryMix(uvs[0], uvs[1], uvs[2], bary);
//TODO or not TODO? const vec2 texture_uv = texture_uv_stationary + push_constants.time * kusok.uv_speed;

View File

@ -33,6 +33,16 @@ static struct {
int rtable[MOD_FRAMES][MOD_FRAMES];
} g_brush;
#define MAX_BRUSH_ENTITIES_PREV_STATES 1024
typedef struct {
matrix4x4 prev_model_transform;
float prev_time;
} brush_entity_prev_state_t;
static brush_entity_prev_state_t g_brush_prev_states[MAX_BRUSH_ENTITIES_PREV_STATES];
void VK_InitRandomTable( void )
{
int tu, tv;
@ -82,6 +92,7 @@ static void EmitWaterPolys( const cl_entity_t *ent, const msurface_t *warp, qboo
{
const float time = gpGlobals->time;
float *v, nv, waveHeight;
float prev_nv, prev_time;
float s, t, os, ot;
glpoly_t *p;
int i;
@ -90,6 +101,11 @@ static void EmitWaterPolys( const cl_entity_t *ent, const msurface_t *warp, qboo
uint16_t *indices;
r_geometry_buffer_lock_t buffer;
if (ent->index < MAX_BRUSH_ENTITIES_PREV_STATES) {
prev_time = g_brush_prev_states[ent->index].prev_time;
g_brush_prev_states[ent->index].prev_time = time;
} else gEngine.Con_Printf(S_ERROR "Brush entities previous frame states pool is overflow, increase it. Index is %s\n", ent->index );
#define MAX_WATER_VERTICES 16
vk_vertex_t poly_vertices[MAX_WATER_VERTICES];
@ -135,6 +151,10 @@ static void EmitWaterPolys( const cl_entity_t *ent, const msurface_t *warp, qboo
nv = r_turbsin[(int)(time * 160.0f + v[1] + v[0]) & 255] + 8.0f;
nv = (r_turbsin[(int)(v[0] * 5.0f + time * 171.0f - v[1]) & 255] + 8.0f ) * 0.8f + nv;
nv = nv * waveHeight + v[2];
prev_nv = r_turbsin[(int)(prev_time * 160.0f + v[1] + v[0]) & 255] + 8.0f;
prev_nv = (r_turbsin[(int)(v[0] * 5.0f + prev_time * 171.0f - v[1]) & 255] + 8.0f ) * 0.8f + prev_nv;
prev_nv = prev_nv * waveHeight + v[2];
}
else nv = v[2];
@ -151,6 +171,10 @@ static void EmitWaterPolys( const cl_entity_t *ent, const msurface_t *warp, qboo
poly_vertices[i].pos[1] = v[1];
poly_vertices[i].pos[2] = nv;
poly_vertices[i].prev_pos[0] = v[0];
poly_vertices[i].prev_pos[1] = v[1];
poly_vertices[i].prev_pos[2] = prev_nv;
poly_vertices[i].gl_tc[0] = s;
poly_vertices[i].gl_tc[1] = t;
@ -265,9 +289,17 @@ void XVK_DrawWaterSurfaces( const cl_entity_t *ent )
EmitWaterPolys( ent, surf, false );
}
int entity_id = ent->index;
if (entity_id < MAX_BRUSH_ENTITIES_PREV_STATES)
Matrix4x4_Copy( *VK_RenderGetLastFrameTransform(), g_brush_prev_states[entity_id].prev_model_transform );
// submit as dynamic model
VK_RenderModelDynamicCommit();
if (entity_id < MAX_BRUSH_ENTITIES_PREV_STATES)
Matrix4x4_Copy( g_brush_prev_states[entity_id].prev_model_transform, *VK_RenderGetLastFrameTransform() );
// TODO:
// - upload water geometry only once, animate in compute/vertex shader
}
@ -360,8 +392,18 @@ void VK_BrushModelDraw( const cl_entity_t *ent, int render_mode, const matrix4x4
}
}
int entity_id = ent->index;
if (entity_id < MAX_BRUSH_ENTITIES_PREV_STATES) {
Matrix4x4_Copy( bmodel->render_model.prev_transform,
g_brush_prev_states[entity_id].prev_model_transform );
} else gEngine.Con_Printf(S_ERROR "Brush entities previous frame states pool is overflow, increase it. Index is %s\n", ent->index );
bmodel->render_model.render_mode = render_mode;
VK_RenderModelDraw(ent, &bmodel->render_model);
if (entity_id >= 0 && entity_id < MAX_BRUSH_ENTITIES_PREV_STATES) {
Matrix4x4_Copy( g_brush_prev_states[entity_id].prev_model_transform, bmodel->render_model.prev_transform );
}
}
static qboolean renderableSurface( const msurface_t *surf, int i ) {
@ -576,6 +618,10 @@ static qboolean loadBrushSurfaces( model_sizes_t sizes, const model_t *mod ) {
{in_vertex->position[0], in_vertex->position[1], in_vertex->position[2]},
};
vertex.prev_pos[0] = in_vertex->position[0];
vertex.prev_pos[1] = in_vertex->position[1];
vertex.prev_pos[2] = in_vertex->position[2];
float s = DotProduct( in_vertex->position, surf->texinfo->vecs[0] ) + surf->texinfo->vecs[0][3];
float t = DotProduct( in_vertex->position, surf->texinfo->vecs[1] ) + surf->texinfo->vecs[1][3];

View File

@ -15,6 +15,7 @@
typedef struct vk_vertex_s {
// TODO padding needed for storage buffer reading, figure out how to fix in GLSL/SPV side
vec3_t pos; float p0_;
vec3_t prev_pos; float p01_;
vec3_t normal; uint32_t flags;
vec3_t tangent; uint32_t p1_;
vec2_t gl_tc; //float p2_[2];

View File

@ -301,6 +301,7 @@ vk_ray_model_t* VK_RayModelCreate( vk_ray_model_init_t args ) {
const vec3_t color = {1, 1, 1};
applyMaterialToKusok(kusochki + i, mg, color, false);
Matrix4x4_LoadIdentity(kusochki[i].prev_transform);
}
R_VkStagingUnlock(kusok_staging.handle);
@ -493,6 +494,8 @@ void VK_RayFrameAddModel( vk_ray_model_t *model, const vk_render_model_t *render
for (int i = 0; i < render_model->num_geometries; ++i) {
const vk_render_geometry_t *geom = render_model->geometries + i;
applyMaterialToKusok(kusochki + i, geom, color, HACK_reflective);
Matrix4x4_Copy((kusochki + i)->prev_transform, render_model->prev_transform);
}
/* gEngine.Con_Reportf("model %s: geom=%d kuoffs=%d kustoff=%d kustsz=%d sthndl=%d\n", */

View File

@ -87,6 +87,7 @@ static qboolean createPipelines( void )
{.binding = 0, .location = 3, .format = VK_FORMAT_R32G32_SFLOAT, .offset = offsetof(vk_vertex_t, lm_tc)},
{.binding = 0, .location = 4, .format = VK_FORMAT_R8G8B8A8_UNORM, .offset = offsetof(vk_vertex_t, color)},
{.binding = 0, .location = 5, .format = VK_FORMAT_R32_UINT, .offset = offsetof(vk_vertex_t, flags)},
{.binding = 0, .location = 6, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = offsetof(vk_vertex_t, prev_pos)},
};
const vk_shader_stage_t shader_stages[] = {
@ -682,6 +683,9 @@ void VK_RenderModelDraw( const cl_entity_t *ent, vk_render_model_t* model ) {
if (g_render_state.current_frame_is_ray_traced) {
VK_RayFrameAddModel(model->ray_model, model, (const matrix3x4*)g_render_state.model, g_render_state.dirty_uniform_data.color, ent ? ent->curstate.rendercolor : (color24){255,255,255});
// store current transform here before it puts to entity history
Matrix4x4_Copy( model->prev_transform, g_render_state.model );
return;
}
@ -747,6 +751,10 @@ static struct {
vk_render_geometry_t geometries[MAX_DYNAMIC_GEOMETRY];
} g_dynamic_model = {0};
matrix4x4 *VK_RenderGetLastFrameTransform() {
return &g_dynamic_model.model.prev_transform;
}
void VK_RenderModelDynamicBegin( int render_mode, const char *debug_name_fmt, ... ) {
va_list argptr;
va_start( argptr, debug_name_fmt );

View File

@ -82,6 +82,9 @@ typedef struct vk_render_model_s {
struct vk_ray_model_s *ray_model;
struct rt_light_add_polygon_s *polylights;
int polylights_count;
// previous frame ObjectToWorld (model) matrix
matrix4x4 prev_transform;
} vk_render_model_t;
qboolean VK_RenderModelInit( vk_render_model_t* model );
@ -100,3 +103,5 @@ void VK_RenderEnd( VkCommandBuffer cmdbuf );
void VK_RenderEndRTX( VkCommandBuffer cmdbuf, VkImageView img_dst_view, VkImage img_dst, uint32_t w, uint32_t h );
void VK_Render_FIXME_Barrier( VkCommandBuffer cmdbuf );
matrix4x4* VK_RenderGetLastFrameTransform();

View File

@ -156,6 +156,13 @@ static void prepareUniformBuffer( const vk_ray_frame_render_args_t *args, int fr
Matrix4x4_Invert_Full(view_inv, *args->view);
Matrix4x4_ToArrayFloatGL(view_inv, (float*)ubo->inv_view);
// last frame matrices
static matrix4x4 prev_inv_proj, prev_inv_view;
Matrix4x4_ToArrayFloatGL(prev_inv_proj, (float*)ubo->prev_inv_proj);
Matrix4x4_ToArrayFloatGL(prev_inv_view, (float*)ubo->prev_inv_view);
Matrix4x4_Copy(prev_inv_view, view_inv);
Matrix4x4_Copy(prev_inv_proj, proj_inv);
ubo->ray_cone_width = atanf((2.0f*tanf(DEG2RAD(fov_angle_y) * 0.5f)) / (float)FRAME_HEIGHT);
ubo->random_seed = (uint32_t)gEngine.COM_RandomLong(0, INT32_MAX);
}

View File

@ -47,6 +47,11 @@ typedef struct sortedmesh_s
int flags; // face flags
} sortedmesh_t;
typedef struct {
matrix3x4 worldtransform[MAXSTUDIOBONES];
matrix4x4 prev_transform;
} studio_entity_prev_state_t;
typedef struct
{
double time;
@ -74,6 +79,8 @@ typedef struct
vec3_t verts[MAXSTUDIOVERTS];
vec3_t norms[MAXSTUDIOVERTS];
vec3_t prev_verts[MAXSTUDIOVERTS]; // last frame state for motion vectors
// lighting state
float ambientlight;
float shadelight;
@ -118,6 +125,9 @@ static cvar_t *cl_himodels;
static r_studio_interface_t *pStudioDraw;
static studio_draw_state_t g_studio; // global studio state
#define MAX_ENTITIES_PREV_STATES_STUDIO 1024
static studio_entity_prev_state_t g_entity_prev_states[MAX_ENTITIES_PREV_STATES_STUDIO];
// global variables
static qboolean m_fDoRemap;
mstudiomodel_t *m_pSubModel;
@ -254,7 +264,7 @@ static qboolean R_StudioComputeBBox( vec3_t bbox[8] )
return true; // visible
}
void R_StudioComputeSkinMatrix( mstudioboneweight_t *boneweights, matrix3x4 result )
void R_StudioComputeSkinMatrix( mstudioboneweight_t *boneweights, matrix3x4 *worldtransform, matrix3x4 result )
{
float flWeight0, flWeight1, flWeight2, flWeight3;
int i, numbones = 0;
@ -268,10 +278,10 @@ void R_StudioComputeSkinMatrix( mstudioboneweight_t *boneweights, matrix3x4 resu
if( numbones == 4 )
{
vec4_t *boneMat0 = (vec4_t *)g_studio.worldtransform[boneweights->bone[0]];
vec4_t *boneMat1 = (vec4_t *)g_studio.worldtransform[boneweights->bone[1]];
vec4_t *boneMat2 = (vec4_t *)g_studio.worldtransform[boneweights->bone[2]];
vec4_t *boneMat3 = (vec4_t *)g_studio.worldtransform[boneweights->bone[3]];
vec4_t *boneMat0 = (vec4_t *)worldtransform[boneweights->bone[0]];
vec4_t *boneMat1 = (vec4_t *)worldtransform[boneweights->bone[1]];
vec4_t *boneMat2 = (vec4_t *)worldtransform[boneweights->bone[2]];
vec4_t *boneMat3 = (vec4_t *)worldtransform[boneweights->bone[3]];
flWeight0 = boneweights->weight[0] / 255.0f;
flWeight1 = boneweights->weight[1] / 255.0f;
flWeight2 = boneweights->weight[2] / 255.0f;
@ -295,9 +305,9 @@ void R_StudioComputeSkinMatrix( mstudioboneweight_t *boneweights, matrix3x4 resu
}
else if( numbones == 3 )
{
vec4_t *boneMat0 = (vec4_t *)g_studio.worldtransform[boneweights->bone[0]];
vec4_t *boneMat1 = (vec4_t *)g_studio.worldtransform[boneweights->bone[1]];
vec4_t *boneMat2 = (vec4_t *)g_studio.worldtransform[boneweights->bone[2]];
vec4_t *boneMat0 = (vec4_t *)worldtransform[boneweights->bone[0]];
vec4_t *boneMat1 = (vec4_t *)worldtransform[boneweights->bone[1]];
vec4_t *boneMat2 = (vec4_t *)worldtransform[boneweights->bone[2]];
flWeight0 = boneweights->weight[0] / 255.0f;
flWeight1 = boneweights->weight[1] / 255.0f;
flWeight2 = boneweights->weight[2] / 255.0f;
@ -320,8 +330,8 @@ void R_StudioComputeSkinMatrix( mstudioboneweight_t *boneweights, matrix3x4 resu
}
else if( numbones == 2 )
{
vec4_t *boneMat0 = (vec4_t *)g_studio.worldtransform[boneweights->bone[0]];
vec4_t *boneMat1 = (vec4_t *)g_studio.worldtransform[boneweights->bone[1]];
vec4_t *boneMat0 = (vec4_t *)worldtransform[boneweights->bone[0]];
vec4_t *boneMat1 = (vec4_t *)worldtransform[boneweights->bone[1]];
flWeight0 = boneweights->weight[0] / 255.0f;
flWeight1 = boneweights->weight[1] / 255.0f;
flTotal = flWeight0 + flWeight1;
@ -343,7 +353,7 @@ void R_StudioComputeSkinMatrix( mstudioboneweight_t *boneweights, matrix3x4 resu
}
else
{
Matrix3x4_Copy( result, g_studio.worldtransform[boneweights->bone[0]] );
Matrix3x4_Copy( result, worldtransform[boneweights->bone[0]] );
}
}
@ -1062,6 +1072,26 @@ static void R_StudioSaveBones( void )
}
}
/*
====================
SaveTransformsForNextFrame
====================
*/
static void R_StudioSaveTransformsForNextFrame( matrix3x4* bones_transforms )
{
if (RI.currententity->index >= MAX_ENTITIES_PREV_STATES_STUDIO)
return;
studio_entity_prev_state_t *prev_state = &g_entity_prev_states[RI.currententity->index];
for( int i = 0; i < m_pStudioHeader->numbones; i++ )
{
Matrix3x4_Copy(prev_state->worldtransform[i], bones_transforms[i]);
}
}
/*
====================
StudioBuildNormalTable
@ -1946,6 +1976,7 @@ static void R_StudioDrawNormalMesh( short *ptricmds, vec3_t *pstudionorms, float
*dst_vtx = (vk_vertex_t){0};
VectorCopy(g_studio.verts[ptricmds[0]], dst_vtx->pos);
VectorCopy(g_studio.prev_verts[ptricmds[0]], dst_vtx->prev_pos);
VectorCopy(g_studio.norms[ptricmds[0]], dst_vtx->normal);
dst_vtx->lm_tc[0] = dst_vtx->lm_tc[1] = 0.f;
@ -2127,6 +2158,14 @@ static void R_StudioDrawPoints( void )
pskinref = (short *)((byte *)m_pStudioHeader + m_pStudioHeader->skinindex);
if( m_skinnum != 0 ) pskinref += (m_skinnum * m_pStudioHeader->numskinref);
studio_entity_prev_state_t *prev_frame_state = &g_entity_prev_states[RI.currententity->index];
if (RI.currententity->index >= MAX_ENTITIES_PREV_STATES_STUDIO)
{
gEngine.Con_Printf(S_ERROR "Studio entities previous frame states pool is overflow, increase it. Index is %s\n", RI.currententity->index);
prev_frame_state = &g_entity_prev_states[MAX_ENTITIES_PREV_STATES_STUDIO - 1]; // fallback to last element
}
if( FBitSet( m_pStudioHeader->flags, STUDIO_HAS_BONEWEIGHTS ) && m_pSubModel->blendvertinfoindex != 0 && m_pSubModel->blendnorminfoindex != 0 )
{
mstudioboneweight_t *pvertweight = (mstudioboneweight_t *)((byte *)m_pStudioHeader + m_pSubModel->blendvertinfoindex);
@ -2135,24 +2174,35 @@ static void R_StudioDrawPoints( void )
for( i = 0; i < m_pSubModel->numverts; i++ )
{
R_StudioComputeSkinMatrix( &pvertweight[i], skinMat );
R_StudioComputeSkinMatrix( &pvertweight[i], g_studio.worldtransform, skinMat );
Matrix3x4_VectorTransform( skinMat, pstudioverts[i], g_studio.verts[i] );
R_LightStrength( pvertbone[i], pstudioverts[i], g_studio.lightpos[i] );
}
for( i = 0; i < m_pSubModel->numverts; i++ )
{
R_StudioComputeSkinMatrix( &pvertweight[i], prev_frame_state->worldtransform, skinMat );
Matrix3x4_VectorTransform( skinMat, pstudioverts[i], g_studio.prev_verts[i] );
}
for( i = 0; i < m_pSubModel->numnorms; i++ )
{
R_StudioComputeSkinMatrix( &pnormweight[i], skinMat );
R_StudioComputeSkinMatrix( &pnormweight[i], g_studio.worldtransform, skinMat );
Matrix3x4_VectorRotate( skinMat, pstudionorms[i], g_studio.norms[i] );
}
R_StudioSaveTransformsForNextFrame (g_studio.worldtransform );
}
else
{
for( i = 0; i < m_pSubModel->numverts; i++ )
{
Matrix3x4_VectorTransform( g_studio.bonestransform[pvertbone[i]], pstudioverts[i], g_studio.verts[i] );
Matrix3x4_VectorTransform( prev_frame_state->worldtransform[pvertbone[i]], pstudioverts[i], g_studio.prev_verts[i] );
R_LightStrength( pvertbone[i], pstudioverts[i], g_studio.lightpos[i] );
}
R_StudioSaveTransformsForNextFrame( g_studio.bonestransform );
}
// generate shared normals for properly scaling glowing shell
@ -2278,7 +2328,17 @@ static void R_StudioDrawPoints( void )
*/
}
int entity_id = RI.currententity->index;
if (entity_id < MAX_ENTITIES_PREV_STATES_STUDIO) {
Matrix4x4_Copy( *VK_RenderGetLastFrameTransform(), g_entity_prev_states[entity_id].prev_transform );
} else gEngine.Con_Printf(S_ERROR "Studio entities last states pool is overflow, increase it. Index is %s\n", entity_id );
VK_RenderModelDynamicCommit();
if (entity_id < MAX_ENTITIES_PREV_STATES_STUDIO) {
Matrix4x4_Copy( g_entity_prev_states[entity_id].prev_transform, *VK_RenderGetLastFrameTransform() );
}
}
static void R_StudioSetRemapColors( int newTop, int newBottom )