Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
83 lines
4.2 KiB
Python
83 lines
4.2 KiB
Python
"""Execute the production pose conversion against independent body/camera timing.
|
|
|
|
This isolates frame-composition error; it does not measure live network jitter
|
|
or the engine's replicated weapon animation.
|
|
"""
|
|
from policy_harness import ROOT, body, run
|
|
|
|
source = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverPlayerView.c'
|
|
methods = '\n'.join(body(source, signature) for signature in
|
|
('static void Encode(', 'static void Decode(')).replace('out vector', 'vector')
|
|
# Ensure both actual relay ends use the tested conversion, including scoped poses.
|
|
publisher = body(source, 'void ERR_PublishPlayerView(')
|
|
reader = body(source, 'bool ERR_ReadPlayerView(')
|
|
assert publisher.index('ERR_ObserverScope.Sample(') < publisher.index('ERR_ObserverViewRules.Encode(world, view, pose)')
|
|
assert 'ERR_ObserverViewRules.Decode(world, pose, transform)' in reader
|
|
assert 'MatrixInvMultiply4' not in publisher and 'MatrixMultiply4' not in reader
|
|
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <iostream>
|
|
#include <algorithm>
|
|
struct vector {
|
|
double x=0,y=0,z=0;
|
|
vector operator+(vector b)const{return{x+b.x,y+b.y,z+b.z};}
|
|
vector operator-(vector b)const{return{x-b.x,y-b.y,z-b.z};}
|
|
vector operator*(double n)const{return{x*n,y*n,z*n};}
|
|
};
|
|
double dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}
|
|
double length(vector a){return std::sqrt(dot(a,a));}
|
|
bool close(vector a,vector b){return length(a-b)<1e-8;}
|
|
vector rotate(vector* m,vector v){return m[0]*v.x+m[1]*v.y+m[2]*v.z;}
|
|
vector inverse(vector* m,vector v){return{dot(m[0],v),dot(m[1],v),dot(m[2],v)};}
|
|
void yaw(double angle,vector origin,vector* m){
|
|
m[0]={std::cos(angle),0,-std::sin(angle)};m[1]={0,1,0};
|
|
m[2]={std::sin(angle),0,std::cos(angle)};m[3]=origin;
|
|
}
|
|
struct Rules {'''+methods+r'''};
|
|
int main(){
|
|
vector owner[4],proxy[4],view[4],pose[4],result[4];
|
|
// Rotate proxy independently through a whole turn, with lean/roll and eye
|
|
// offsets. The receiver follows root translation but cannot inject root yaw.
|
|
for(int a=-180;a<=180;a+=15)for(int b=-180;b<=180;b+=15){
|
|
yaw(a*.0174532925199433,{15000,300,16000},owner);
|
|
yaw(b*.0174532925199433,{15002,301,16003},proxy);
|
|
yaw(.7,{15000.3,301.6,16000.2},view);
|
|
auto right=view[0],up=view[1];view[0]=right*.8+up*.6;view[1]=up*.8-right*.6;
|
|
Rules::Encode(owner,view,pose);Rules::Decode(proxy,pose,result);
|
|
for(int axis=0;axis<3;axis++)assert(close(result[axis],view[axis]));
|
|
assert(close(result[3]-proxy[3],view[3]-owner[3]));
|
|
Rules::Decode(owner,pose,result);for(int axis=0;axis<4;axis++)assert(close(result[axis],view[axis]));
|
|
}
|
|
// Reproduce the reported contrast: a turning root has phase error between
|
|
// streams; a strafing root stays fixed even as camera aim continues turning.
|
|
for(bool strafing : {false,true}){
|
|
double oldMaxAngle=0,oldMaxEye=0,newMaxAngle=0,newMaxEye=0;
|
|
for(int frame=0;frame<600;frame++){
|
|
double t=frame/60.0,delay=.15;
|
|
double rootYaw=strafing?0:std::sin(t*2)*.6;
|
|
double proxyYaw=strafing?0:std::sin((t+delay)*2)*.6;
|
|
vector origin{t*2,0,0},remoteOrigin{(t+delay)*2,0,0};
|
|
yaw(rootYaw,origin,owner);yaw(proxyYaw,remoteOrigin,proxy);
|
|
yaw(t*.25,origin+rotate(owner,{.15,1.6,.2}),view);
|
|
// Previous encoding and reconstruction: inverse owner then current proxy.
|
|
vector oldForward=rotate(proxy,inverse(owner,view[2]));
|
|
vector oldOffset=rotate(proxy,inverse(owner,view[3]-owner[3]));
|
|
Rules::Encode(owner,view,pose);Rules::Decode(proxy,pose,result);
|
|
auto angle=[](vector a,vector b){return std::acos(std::clamp(dot(a,b),-1.0,1.0));};
|
|
oldMaxAngle=std::max(oldMaxAngle,angle(oldForward,view[2]));
|
|
oldMaxEye=std::max(oldMaxEye,length(oldOffset-(view[3]-owner[3])));
|
|
newMaxAngle=std::max(newMaxAngle,angle(result[2],view[2]));
|
|
newMaxEye=std::max(newMaxEye,length((result[3]-proxy[3])-(view[3]-owner[3])));
|
|
}
|
|
assert(newMaxAngle<1e-7&&newMaxEye<1e-8);
|
|
if(strafing)assert(oldMaxAngle<1e-7&&oldMaxEye<1e-8);
|
|
else assert(oldMaxAngle>.15&&oldMaxEye>.04);
|
|
std::cout<<"strafing="<<strafing<<" old_frame_error_rad="<<oldMaxAngle<<" corrected="<<newMaxAngle
|
|
<<" old_eye_error_m="<<oldMaxEye<<" corrected="<<newMaxEye<<"\n";
|
|
}
|
|
}
|
|
''')
|
|
print('PASS: actual pose conversion removes independently replicated root rotation from aim/eye offset, preserves roll and follows body translation')
|