// math stuff Struct SWWMProjectionData { double tanfovx, tanfovy; Vector3 viewpos, z, x, y; int viewx, viewy, vieww, viewh; } extend Class SWWMUtility { // cache some data that requires trig and quat math static void PrepareProjData( SWWMProjectionData &d, Vector3 viewpos, double angle, double pitch, double roll, double fov ) { // store for internal use d.viewpos = viewpos; // precalc vfov/hfov tangents // (vfov in gzdoom has a small quirk to it) double aspect = Screen.GetAspectRatio(); double fovratio = (aspect>=1.3)?1.333333:aspect; d.tanfovy = tan(clamp(fov,5,170)/2.)/fovratio; d.tanfovx = d.tanfovy*aspect; // precalc view-space axes // (don't forget pixel stretch, very important) Quat r = Quat.FromAngles(angle,pitch,roll); d.z = r*(1.,0.,0.); d.x = r*(0.,1.,0.); d.y = r*(0.,0.,level.pixelstretch); // precalc view origin and clip int sblocks = CVar.FindCVar('screenblocks').GetInt(); let [viewx, viewy, vieww, viewh] = Screen.GetViewWindow(); int sh = Screen.GetHeight(); int h = sh; if ( sblocks < 10 ) h = (sblocks*sh/10)&~7; int bottom = sh-(h+viewy-((h-viewh)/2)); d.viewx = viewx; d.viewy = sh-bottom-h; d.vieww = vieww; d.viewh = h; } // simple projection without matrices, translated from old UnrealScript work // bFast: quit early if point is behind screen, for cases where behind-view coords are not really needed static Vector3 ProjectPoint( SWWMProjectionData d, Vector3 worldpos, bool bFast = true ) { Vector3 tdir = worldpos-d.viewpos; // early bail, skip behind-view points if ( bFast && (d.z dot tdir <= 0.) ) return (0.,0.,0.); double dist = tdir.length(); // points are pretty much equal, skip projection if ( dist <= double.epsilon ) return (0.,0.,0.); tdir /= dist; Vector3 dir = d.z*(tdir dot d.z); // I don't understand this math, but it works? Vector3 xy = tdir-dir; double dx = xy dot d.x; double dy = xy dot d.y; double dlen = dir.length(); // guard against division by zero here? if ( dlen <= double.epsilon ) return (0.,0.,0.); double tanx = dx/dlen; double tany = dy/dlen; return (1.-tanx/d.tanfovx,1.-tany/d.tanfovy,dir dot d.z); } static Vector2 NDCToViewport( SWWMProjectionData d, Vector3 cpos ) { return (d.viewx+(cpos.x*.5*d.vieww),d.viewy+(cpos.y*.5*d.viewh)); } // checks if a point is inside the viewport static bool TestScreenBounds( SWWMProjectionData d, Vector2 vpos ) { return ((vpos.x == clamp(vpos.x,d.viewx,d.viewx+d.vieww)) && (vpos.y == clamp(vpos.y,d.viewy,d.viewy+d.viewh))); } // less code duplication static void AdjustClean_1( double &x, double &y ) { x = (x-160)*CleanXFac_1+(Screen.GetWidth()*.5); y = (y-100)*CleanYFac_1+(Screen.GetHeight()*.5); } static void AdjustClean_1x( double &x ) { x = (x-160)*CleanXFac_1+(Screen.GetWidth()*.5); } static void AdjustClean_1y( double &y ) { y = (y-100)*CleanYFac_1+(Screen.GetHeight()*.5); } // Vector/Axis utility functions static Vector3 Vec3FromAngles( double angle, double pitch ) { return (cos(angle)*cos(pitch),sin(angle)*cos(pitch),-sin(pitch)); } static Vector3 CircleOffset( Vector3 y, Vector3 z, double angle, double radius ) { return y*cos(angle)*radius+z*sin(angle)*radius; } static Vector3 ConeSpread( Vector3 x, Vector3 y, Vector3 z, double angle, double spread ) { return (x+y*cos(angle)*spread+z*sin(angle)*spread).unit(); } static Vector3 RotateVector3( Vector3 v, double angle ) { Vector2 v2d = Actor.RotateVector(v.xy,angle); return (v2d.x,v2d.y,v.z); } static Vector3 AngleToVector3( double angle, double length = 1. ) { Vector2 v2d = Actor.AngleToVector(angle,length); return (v2d.x,v2d.y,0); } // new GetAxes static Vector3, Vector3, Vector3 GetAxes( double angle, double pitch, double roll ) { Quat r = Quat.FromAngles(angle,pitch,roll); return r*(1,0,0), r*(0,-1,0), r*(0,0,1); } static double Normalize180( double angle ) { angle = ((angle%360.)+360.)%360.; return (angle>180.)?(angle-360.):(angle); } static double, double, double ToAngles( Quat q ) { double angle = 0., pitch = 0., roll = 0.; double stest = q.z*q.x-q.w*q.y; double angY = 2.*(q.w*q.z+q.x*q.y); double angX = 1.-2.*(q.y*q.y+q.z*q.z); if ( stest < -.4999995 ) { angle = atan2(angY,angX); pitch = 90.; roll = Normalize180(angle+(2.*atan2(q.x,q.w))); } else if ( stest > .4999995 ) { angle = atan2(angY,angX); pitch = -90.; roll = Normalize180(angle+(2.*atan2(q.x,q.w))); } else { angle = atan2(angY,angX); pitch = -asin(2.*stest); roll = atan2(2.*(q.w*q.x+q.y*q.z),1.-2.*(q.x*q.x+q.y*q.y)); } return angle, pitch, roll; } // for aiming and shooting static Vector3 GetPlayerViewDir( Actor player ) { Quat r = Quat.FromAngles(player.angle+player.viewangle,player.pitch+player.viewpitch,player.roll+player.viewroll); return r*(1,0,0); } static play Vector3 GetPlayerAimDir( Actor player ) { FTranslatedLineTarget t; double pitch = player.BulletSlope(t); Quat r; if ( !t.linetarget ) r = Quat.FromAngles(player.angle+player.viewangle,player.pitch+player.viewpitch,player.roll+player.viewroll); else r = Quat.FromAngles(player.angle+player.viewangle,pitch,player.roll+player.viewroll); return r*(1,0,0); } static Vector3, Vector3, Vector3 GetPlayerAxes( Actor player ) { Quat r = Quat.FromAngles(player.angle+player.viewangle,player.pitch+player.viewpitch,player.roll+player.viewroll); return r*(1,0,0), r*(0,-1,0), r*(0,0,1); } static play Vector3, Vector3, Vector3 GetPlayerAxesAutoAimed( Actor player ) { FTranslatedLineTarget t; double pitch = player.BulletSlope(t); Quat r; if ( !t.linetarget ) r = Quat.FromAngles(player.angle+player.viewangle,player.pitch+player.viewpitch,player.roll+player.viewroll); else r = Quat.FromAngles(player.angle+player.viewangle,pitch,player.roll+player.viewroll); return r*(1,0,0), r*(0,-1,0), r*(0,0,1); } static Vector3 GetPlayerEye( Actor player ) { if ( !player.viewpos ) return player.Vec2OffsetZ(0,0,player.player.viewz); if ( player.viewpos.flags&VPSF_ABSOLUTEPOS ) return player.viewpos.offset; Vector3 origin = player.Vec2OffsetZ(0,0,player.player.viewz); if ( player.viewpos.flags&VPSF_ABSOLUTEOFFSET ) return level.Vec3Offset(origin,player.viewpos.offset); Quat r = Quat.FromAngles(player.angle,player.pitch,player.roll); // viewangles are not used return level.Vec3Offset(origin,r*player.viewpos.offset); } static Vector3 GetFireOffset( Actor player, double x, double y, double z ) { Vector3 origin = GetPlayerEye(player); Quat r = Quat.FromAngles(player.angle+player.viewangle,player.pitch+player.viewpitch,player.roll+player.viewroll); return level.Vec3Offset(origin,r*(x,-y,z)); } // thanks zscript static double fract( double a ) { return a-floor(a); } static double lerp( double a, double b, double theta ) { return a*(1.-theta)+b*theta; } static Vector3 LerpVector3( Vector3 a, Vector3 b, double theta ) { return a*(1.-theta)+b*theta; } static Vector2 LerpVector2( Vector2 a, Vector2 b, double theta ) { return a*(1.-theta)+b*theta; } static Color LerpColor( Color a, Color b, double theta ) { Color c = Color( int(a.a*(1.-theta)+b.a*theta), int(a.r*(1.-theta)+b.r*theta), int(a.g*(1.-theta)+b.g*theta), int(a.b*(1.-theta)+b.b*theta) ); return c; } static double MapRange( double amin, double amax, double bmin, double bmax, double theta ) { return bmin+(theta-amin)*(bmax-bmin)/(amax-amin); } // this can probably be simplified, but I'm lazy static Vector3 HSVtoRGB( Vector3 hsv ) { Vector3 p; p.x = abs(fract(hsv.x+1.)*6.-3.); p.y = abs(fract(hsv.x+(2./3.))*6.-3.); p.z = abs(fract(hsv.x+(1./3.))*6.-3.); Vector3 p2; p2.x = (1.-hsv.y)+clamp(p.x-1.,0.,1.)*hsv.y; p2.y = (1.-hsv.y)+clamp(p.y-1.,0.,1.)*hsv.y; p2.z = (1.-hsv.y)+clamp(p.z-1.,0.,1.)*hsv.y; return p2*hsv.z; } // "fast" exponentiation with integer exponents using squaring static double IntPowF( double base, int exp ) { if ( exp < 0 ) return 1./IntPowF(base,-exp); if ( exp == 0 ) return 1.; double rslt = 1.; while ( exp ) { if ( exp&1 ) rslt *= base; exp >>= 1; base *= base; } return rslt; } static int IntPow( int base, int exp ) { if ( exp < 0 ) return int(1./IntPow(base,-exp)); if ( exp == 0 ) return 1; int rslt = 1; while ( exp ) { if ( exp&1 ) rslt *= base; exp >>= 1; base *= base; } return rslt; } // pitch from one actor's eyes to another based on a specific fraction of its height // meant for player view, Actor.PitchTo is recommended for more "general" situations static double PitchTo( Actor a, Actor b, double hfact = 1. ) { if ( !a || !b ) return 0; Vector3 thispos = a.player?a.Vec2OffsetZ(0,0,a.player.viewz):a.Vec3Offset(0,0,a.GetCameraHeight()-a.floorclip); Vector3 otherpos = b.Vec3Offset(0,0,b.height*hfact); Vector3 diff = level.Vec3Diff(thispos,otherpos); return -atan2(diff.z,diff.xy.length()); } // box intersection check, for collision detection static bool BoxIntersect( Actor a, Actor b, Vector3 ofs = (0,0,0), int pad = 0 ) { Vector3 diff = level.Vec3Diff(level.Vec3Offset(a.pos,ofs),b.pos); if ( (abs(diff.x) > (a.radius+b.radius+pad)) || (abs(diff.y) > (a.radius+b.radius+pad)) ) return false; if ( (diff.z > a.height+pad) || (diff.z < -(b.height+pad)) ) return false; return true; } // extruded box intersection check, useful when checking things that might be hit along a path static bool ExtrudeIntersect( Actor a, Actor b, Vector3 range, int steps, int pad = 0 ) { if ( steps <= 0 ) return BoxIntersect(a,b,pad:pad); double step = 1./steps; for ( double i=step; i<=1.; i+=step ) { if ( BoxIntersect(a,b,range*i,pad) ) return true; } return false; } // sphere intersection check, useful for proximity detection static bool SphereIntersect( Actor a, Vector3 p, double radius ) { Vector3 ap = p+level.Vec3Diff(p,a.pos); // portal-relative actor position Vector3 amin = ap+(-a.radius,-a.radius,0), amax = ap+(a.radius,a.radius,a.height); double distsq = 0.; if ( p.x < amin.x ) distsq += (amin.x-p.x)*(amin.x-p.x); if ( p.x > amax.x ) distsq += (p.x-amax.x)*(p.x-amax.x); if ( p.y < amin.y ) distsq += (amin.y-p.y)*(amin.y-p.y); if ( p.y > amax.y ) distsq += (p.y-amax.y)*(p.y-amax.y); if ( p.z < amin.z ) distsq += (amin.z-p.z)*(amin.z-p.z); if ( p.z > amax.z ) distsq += (p.z-amax.z)*(p.z-amax.z); return (distsq <= (radius*radius)); } // hitscan exit point calculation given actor, entry point and direction static Vector3 TraceExit( Actor a, Vector3 p, Vector3 d ) { Vector3 ap = p+level.Vec3Diff(p,a.pos); // portal-relative actor position Vector3 amin = ap+(-a.radius,-a.radius,0), amax = ap+(a.radius,a.radius,a.height); Vector3 tmax, div = (1./d.x,1./d.y,1./d.z); if ( div.x < 0 ) tmax.x = (amin.x-p.x)*div.x; else tmax.x = (amax.x-p.x)*div.x; if ( div.y < 0 ) tmax.y = (amin.y-p.y)*div.y; else tmax.y = (amax.y-p.y)*div.y; if ( div.z < 0 ) tmax.z = (amin.z-p.z)*div.z; else tmax.z = (amax.z-p.z)*div.z; return level.Vec3Offset(p,min(min(tmax.x,tmax.y),tmax.z)*d); } // Liang-Barsky line clipping static bool, Vector2, Vector2 LiangBarsky( Vector2 minclip, Vector2 maxclip, Vector2 v0, Vector2 v1 ) { double t0 = 0., t1 = 1.; double xdelta = v1.x-v0.x; double ydelta = v1.y-v0.y; double p, q, r; for ( int i=0;i<4; i++ ) { switch ( i ) { case 0: p = -xdelta; q = -(minclip.x-v0.x); break; case 1: p = xdelta; q = (maxclip.x-v0.x); break; case 2: p = -ydelta; q = -(minclip.y-v0.y); break; case 3: p = ydelta; q = (maxclip.y-v0.y); break; } if ( (p == 0.) && (q<0.) ) return false, (0,0), (0,0); if ( p < 0 ) { r = q/p; if ( r > t1 ) return false, (0,0), (0,0); else if ( r > t0 ) t0 = r; } else if ( p > 0 ) { r = q/p; if ( r < t0 ) return false, (0,0), (0,0); else if ( r < t1 ) t1 = r; } } Vector2 ov0 = v0+(xdelta,ydelta)*t0; Vector2 ov1 = v0+(xdelta,ydelta)*t1; return true, ov0, ov1; } // this check is dependent on the screen aspect ratio, so use only for non-interactive effects static play bool InPlayerFOV( PlayerInfo p, Actor a, double maxdist = 0. ) { double vfov = p.fov*.5; double hfov = atan(Screen.GetAspectRatio()*tan(vfov)); let mo = p.camera; if ( !mo ) return false; Vector3 pp; if ( !a.CheckSight(mo,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) ) return false; if ( mo is 'PlayerPawn' ) pp = mo.Vec2OffsetZ(0,0,PlayerPawn(mo).player.viewz); else pp = mo.Vec3Offset(0,0,mo.GetCameraHeight()); Vector3 sc = level.SphericalCoords(pp,a.pos,(mo.angle,mo.pitch)); if ( (abs(sc.x) > hfov) || (abs(sc.y) > vfov) ) return false; if ( (maxdist > 0.) && (sc.z > maxdist) ) return false; return true; } // ui-friendly version without CheckSight call static bool InPlayerFOVSimple( PlayerInfo p, Actor a, double maxdist = 0. ) { double vfov = p.fov*.5; double hfov = atan(Screen.GetAspectRatio()*tan(vfov)); let mo = p.camera; if ( !mo ) return false; Vector3 pp; if ( mo is 'PlayerPawn' ) pp = mo.Vec2OffsetZ(0,0,PlayerPawn(mo).player.viewz); else pp = mo.Vec3Offset(0,0,mo.GetCameraHeight()); Vector3 sc = level.SphericalCoords(pp,a.pos,(mo.angle,mo.pitch)); if ( (abs(sc.x) > hfov) || (abs(sc.y) > vfov) ) return false; if ( (maxdist > 0.) && (sc.z > maxdist) ) return false; return true; } // calculate angle, pitch and yscale of YBeam based on direction vector and length static double, double, double CalcYBeam( Vector3 dir, double dist ) { if ( level.pixelstretch == 1. ) { // ez modo double angle = atan2(dir.y,dir.x); double pitch = asin(-dir.z)+90; return angle, pitch, dist; } dir.z *= level.pixelstretch; double len = dir.length(); dir /= len; double angle = atan2(dir.y,dir.x); double pitch = asin(-dir.z)+90; double yscale = dist*len; return angle, pitch, yscale; } } // ultrafast PRNG for a bunch of UI stuff // kept here to reduce duplication and to tweak the (very cheap) algo if needed Mixin Class SWWMUIRandom { private ui int rss; private ui void SetUIRandom( int seed ) { rss = seed; } private ui int GetUIRandom() { return (rss = (rss<<1)*35447+(rss/87)); } private ui double RandomShiver() { int sd = GetUIRandom(); return ((abs(sd)%11)-5)*.1; } private ui int RandomFall() { int sd = GetUIRandom(); return ((abs(sd)%22)+10); } private ui double RandomOffset() { int sd = GetUIRandom(); return (abs(sd)&65535)/65535.; } }