LPR - Skating
authorhgn <hgodden00@gmail.com>
Thu, 26 Jan 2023 09:44:19 +0000 (09:44 +0000)
committerhgn <hgodden00@gmail.com>
Thu, 26 Jan 2023 09:44:19 +0000 (09:44 +0000)
maps_src/mp_gridmap.mdl
player_device_skate.h [new file with mode: 0644]
player_device_walk.h
player_interface.h
player_physics.h
rigidbody.h
skaterift.c

index 6bcb5c9581d17d4589b2bc514700f70296984ab8..810f659590d16edcf44b9483b4aa760d445df82c 100644 (file)
Binary files a/maps_src/mp_gridmap.mdl and b/maps_src/mp_gridmap.mdl differ
diff --git a/player_device_skate.h b/player_device_skate.h
new file mode 100644 (file)
index 0000000..af95a7f
--- /dev/null
@@ -0,0 +1,1458 @@
+#ifndef PLAYER_DEVICE_SKATE_H
+#define PLAYER_DEVICE_SKATE_H
+
+#include "player_interface.h"
+#include "skeleton.h"
+#include "player_model.h"
+
+struct player_device_skate
+{
+   struct
+   {
+      enum skate_activity
+      {
+         k_skate_activity_air,
+         k_skate_activity_ground,
+         k_skate_activity_grind
+      }
+      activity,
+      activity_prev;
+
+      float steery,
+            steerx,
+            steery_s,
+            steerx_s,
+            reverse,
+            slip;
+
+      m3x3f velocity_bias,
+            velocity_bias_pstep;
+
+      int lift_frames;
+
+      v3f throw_v;
+      v3f cog_v, cog;
+
+      float grabbing;
+      v2f grab_mouse_delta;
+
+      int charging_jump, jump_dir;
+      float jump_charge;
+      double jump_time;
+
+      double start_push,
+             cur_push;
+
+      v3f vl;
+   }
+   state,
+   state_gate_storage;
+
+   struct land_prediction
+   {
+      v3f   log[50];
+      v3f   n;
+      u32   log_length;
+      float score;
+
+      enum prediction_type
+      {
+         k_prediction_none,
+         k_prediction_land,
+         k_prediction_grind
+      }
+      type;
+
+      u32   colour;
+   }
+   predictions[22];
+   u32 prediction_count;
+
+   /* animation */
+   struct skeleton_anim *anim_stand, *anim_highg, *anim_slide,
+                        *anim_air,
+                        *anim_push,  *anim_push_reverse,
+                        *anim_ollie, *anim_ollie_reverse,
+                        *anim_grabs, *anim_stop;
+   rb_sphere sphere_front, sphere_back;
+   v3f board_offset;
+   v4f board_rotation;
+
+   float blend_slide,
+         blend_z,
+         blend_x,
+         blend_fly,
+         blend_stand,
+         blend_push,
+         blend_jump,
+         blend_airdir;
+
+   v2f wobble;
+
+   float debug_normal_pressure;
+};
+
+VG_STATIC void player_skate_bind( player_interface *player,
+                                  player_attachment *at )
+{
+   struct player_device_skate *s = at->storage;
+   struct player_avatar *av = player->playeravatar;
+   struct skeleton *sk = &av->sk;
+
+   rb_update_transform( &player->rb );
+   s->anim_stand           = skeleton_get_anim( sk, "pose_stand" );
+   s->anim_highg           = skeleton_get_anim( sk, "pose_highg" );
+   s->anim_air             = skeleton_get_anim( sk, "pose_air" );
+   s->anim_slide           = skeleton_get_anim( sk, "pose_slide" );
+   s->anim_push            = skeleton_get_anim( sk, "push" );
+   s->anim_push_reverse    = skeleton_get_anim( sk, "push_reverse" );
+   s->anim_ollie           = skeleton_get_anim( sk, "ollie" );
+   s->anim_ollie_reverse   = skeleton_get_anim( sk, "ollie_reverse" );
+   s->anim_grabs           = skeleton_get_anim( sk, "grabs" );
+}
+
+VG_STATIC void player_skate_pre_update( player_interface *player,
+                                        player_attachment *at )
+{
+}
+
+/* 
+ * Collision detection routines
+ *
+ *
+ */
+
+/*
+ * Does collision detection on a sphere vs world, and applies some smoothing
+ * filters to the manifold afterwards
+ */
+VG_STATIC int skate_collide_smooth( player_interface *player,
+                                    m4x3f mtx, rb_sphere *sphere,
+                                    rb_ct *man )
+{
+   debug_sphere( mtx, sphere->radius, VG__BLACK );
+
+   int len = 0;
+   len = rb_sphere__scene( mtx, sphere, NULL, &world.rb_geo.inf.scene, man );
+
+   for( int i=0; i<len; i++ )
+   {
+      man[i].rba = &player->rb;
+      man[i].rbb = NULL;
+   }
+
+   rb_manifold_filter_coplanar( man, len, 0.05f );
+
+   if( len > 1 )
+   {
+      rb_manifold_filter_backface( man, len );
+      rb_manifold_filter_joint_edges( man, len, 0.05f );
+      rb_manifold_filter_pairs( man, len, 0.05f );
+   }
+   int new_len = rb_manifold_apply_filtered( man, len );
+   if( len && !new_len )
+      len = 1;
+   else
+      len = new_len;
+
+   return len;
+}
+/*
+ * Gets the closest grindable edge to the player within max_dist 
+ */
+VG_STATIC struct grind_edge *skate_collect_grind_edge( v3f p0, v3f p1,
+                                                       v3f c0, v3f c1, 
+                                                       float max_dist )
+{
+   bh_iter it;
+   bh_iter_init( 0, &it );
+
+   boxf region;
+
+   box_init_inf( region );
+   box_addpt( region, p0 );
+   box_addpt( region, p1 );
+   
+   float k_r = max_dist;
+   v3_add( (v3f){ k_r, k_r, k_r}, region[1], region[1] );
+   v3_add( (v3f){-k_r,-k_r,-k_r}, region[0], region[0] );
+
+   float closest = k_r*k_r;
+   struct grind_edge *closest_edge = NULL;
+   
+   int idx;
+   while( bh_next( world.grind_bh, &it, region, &idx ) )
+   {
+      struct grind_edge *edge = &world.grind_edges[ idx ];
+
+      float s,t;
+      v3f pa, pb;
+
+      float d2 = 
+         closest_segment_segment( p0, p1, edge->p0, edge->p1, &s,&t, pa, pb );
+
+      if( d2 < closest )
+      {
+         closest = d2;
+         closest_edge = edge;
+         v3_copy( pa, c0 );
+         v3_copy( pb, c1 );
+      }
+   }
+
+   return closest_edge;
+}
+
+VG_STATIC int skate_grind_collide( player_interface *player, 
+                                   player_attachment *at, rb_ct *contact )
+{
+   v3f p0, p1, c0, c1;
+   v3_muladds( player->rb.co, player->rb.to_world[2],  0.5f, p0 );
+   v3_muladds( player->rb.co, player->rb.to_world[2], -0.5f, p1 );
+   v3_muladds( p0, player->rb.to_world[1], 0.125f-0.15f, p0 );
+   v3_muladds( p1, player->rb.to_world[1], 0.125f-0.15f, p1 );
+
+   float const k_r = 0.25f;
+   struct grind_edge *closest_edge = skate_collect_grind_edge( p0, p1, 
+                                                               c0, c1, k_r );
+
+   if( closest_edge )
+   {
+      v3f delta;
+      v3_sub( c1, c0, delta );
+
+      if( v3_dot( delta, player->rb.to_world[1] ) > 0.0001f )
+      {
+         contact->p = v3_length( delta );
+         contact->type = k_contact_type_edge;
+         contact->element_id = 0;
+         v3_copy( c1, contact->co );
+         contact->rba = NULL;
+         contact->rbb = NULL;
+
+         v3f edge_dir, axis_dir;
+         v3_sub( closest_edge->p1, closest_edge->p0, edge_dir );
+         v3_normalize( edge_dir );
+         v3_cross( (v3f){0.0f,1.0f,0.0f}, edge_dir, axis_dir );
+         v3_cross( edge_dir, axis_dir, contact->n );
+
+         return 1;
+      }
+      else
+         return 0;
+   }
+
+   return 0;
+}
+
+/*
+ *
+ * Prediction system
+ *
+ *
+ */
+
+/* 
+ * Trace a path given a velocity rotation.
+ *
+ * TODO: this MIGHT be worth doing RK4 on the gravity field.
+ */
+VG_STATIC void skate_score_biased_path( v3f co, v3f v, m3x3f vr, 
+                                        struct land_prediction *prediction )
+{
+   float pstep  = VG_TIMESTEP_FIXED * 10.0f;
+   float k_bias = 0.96f;
+
+   v3f pco, pco1, pv;
+   v3_copy( co, pco );
+   v3_muls( v,  k_bias, pv );
+
+   m3x3_mulv( vr, pv, pv );
+   v3_muladds( pco, pv, pstep, pco );
+
+   struct grind_edge *best_grind = NULL;
+   float closest_grind = INFINITY;
+
+   float grind_score   = INFINITY,
+         air_score     = INFINITY;
+
+   prediction->log_length = 0;
+
+   for( int i=0; i<vg_list_size(prediction->log); i++ )
+   {
+      v3_copy( pco, pco1 );
+
+      pv[1] += -k_gravity * pstep;
+
+      m3x3_mulv( vr, pv, pv );
+      v3_muladds( pco, pv, pstep, pco );
+      
+      v3f vdir;
+
+      v3_sub( pco, pco1, vdir );
+
+      float l = v3_length( vdir );
+      v3_muls( vdir, 1.0f/l, vdir );
+
+      v3f c0, c1;
+      struct grind_edge *ge = skate_collect_grind_edge( pco, pco1,
+                                                        c0, c1, 0.4f );
+
+      if( ge && (v3_dot((v3f){0.0f,1.0f,0.0f},vdir) < -0.2f ) )
+      {
+         float d2 = v3_dist2( c0, c1 );
+         if( d2 < closest_grind )
+         {
+            closest_grind = d2;
+            best_grind = ge;
+            grind_score = closest_grind * 0.05f;
+         }
+      }
+
+      v3f n1;
+
+      float t1;
+      int idx = spherecast_world( pco1, pco, 0.4f, &t1, n1 );
+      if( idx != -1 )
+      {
+         v3_copy( n1, prediction->n );
+         air_score = -v3_dot( pv, n1 );
+         
+         u32 vert_index = world.scene_geo->arrindices[ idx*3 ];
+         struct world_material *mat = world_tri_index_material( vert_index );
+
+         /* Bias prediction towords ramps */
+         if( mat->info.flags & k_material_flag_skate_surface )
+            air_score *= 0.1f;
+
+         v3_lerp( pco1, pco, t1, prediction->log[ prediction->log_length ++ ] ); 
+         break;
+      }
+
+      v3_copy( pco, prediction->log[ prediction->log_length ++ ] );
+   }
+
+   if( grind_score < air_score )
+   {
+      prediction->score = grind_score; 
+      prediction->type = k_prediction_grind;
+   }
+   else if( air_score < INFINITY )
+   {
+      prediction->score = air_score;
+      prediction->type = k_prediction_land;
+   }
+   else
+   {
+      prediction->score = INFINITY;
+      prediction->type = k_prediction_none;
+   }
+}
+
+VG_STATIC 
+void player_approximate_best_trajectory( player_interface *player,
+                                         struct player_device_skate *s )
+{
+   float pstep = VG_TIMESTEP_FIXED * 10.0f;
+   float best_velocity_delta = -9999.9f;
+
+   v3f axis;
+   v3_cross( player->rb.to_world[1], player->rb.v, axis );
+   v3_normalize( axis );
+
+   s->prediction_count = 0;
+   m3x3_identity( s->state.velocity_bias );
+
+   float best_vmod   = 0.0f,
+         min_score   =  INFINITY,
+         max_score   = -INFINITY;
+
+   /*
+    * Search a broad selection of futures
+    */
+   for( int m=-3;m<=12; m++ )
+   {
+      struct land_prediction *p = &s->predictions[ s->prediction_count ++ ];
+
+      float vmod = ((float)m / 15.0f)*0.09f;
+
+      m3x3f bias;
+      v4f bias_q;
+
+      q_axis_angle( bias_q, axis, vmod );
+      q_m3x3( bias_q, bias );
+
+      skate_score_biased_path( player->rb.co, player->rb.v, bias, p );
+
+      if( p->type != k_prediction_none )
+      {
+         if( p->score < min_score )
+         {
+            min_score = p->score;
+            best_vmod = vmod;
+         }
+
+         if( p->score > max_score )
+            max_score = p->score;
+      }
+   }
+
+   v4f vr_q;
+   q_axis_angle( vr_q, axis, best_vmod*0.1f );
+   q_m3x3( vr_q, s->state.velocity_bias );
+
+   q_axis_angle( vr_q, axis, best_vmod );
+   q_m3x3( vr_q, s->state.velocity_bias_pstep );
+
+   /*
+    * Logging
+    */
+   for( int i=0; i<s->prediction_count; i ++ )
+   {
+      struct land_prediction *p = &s->predictions[i];
+
+      float l = p->score;
+
+      if( l < 0.0f )
+      {
+         vg_error( "negative score! (%f)\n", l );
+      }
+
+      l -= min_score;
+      l /= (max_score-min_score);
+      l  = 1.0f - l;
+      l *= 255.0f;
+
+      p->colour = l;
+      p->colour <<= 8;
+      p->colour |= 0xff000000;
+   }
+}
+
+/*
+ *
+ * Varius physics models
+ * ------------------------------------------------
+ */
+
+VG_STATIC void skate_apply_grind_model( player_interface *player,
+                                        struct player_device_skate *s,
+                                        rb_ct *manifold, int len )
+{
+   /* FIXME: Queue audio events instead */
+   if( len == 0 )
+   {
+      if( s->state.activity == k_skate_activity_grind )
+      {
+#if 0
+         audio_lock();
+         audio_player_set_flags( &audio_player_extra, 
+                                 AUDIO_FLAG_SPACIAL_3D );
+         audio_player_set_position( &audio_player_extra, player.rb.co );
+         audio_player_set_vol( &audio_player_extra, 20.0f );
+         audio_player_playclip( &audio_player_extra, &audio_board[6] );
+         audio_unlock();
+#endif
+
+         s->state.activity = k_skate_activity_air;
+      }
+      return;
+   }
+
+   v2f steer = { player->input_js1h->axis.value,
+                 player->input_js1v->axis.value };
+   v2_normalize_clamp( steer );
+
+   s->state.steery -= steer[0] * k_steer_air * k_rb_delta;
+   s->state.steerx += steer[1] * s->state.reverse * k_steer_air * k_rb_delta;
+   
+#if 0
+   v4f rotate;
+   q_axis_angle( rotate, player->rb.to_world[0], siX );
+   q_mul( rotate, player.rb.q, player.rb.q );
+#endif
+
+   s->state.slip = 0.0f;
+   s->state.activity = k_skate_activity_grind;
+
+   /* TODO: Compression */
+   v3f up = { 0.0f, 1.0f, 0.0f };
+   float angle = v3_dot( player->rb.to_world[1], up );
+
+   if( fabsf(angle) < 0.99f )
+   {
+      v3f axis; 
+      v3_cross( player->rb.to_world[1], up, axis );
+
+      v4f correction;
+      q_axis_angle( correction, axis, k_rb_delta * 10.0f * acosf(angle) );
+      q_mul( correction, player->rb.q, player->rb.q );
+   }
+
+   float const DOWNFORCE = -k_downforce*1.2f*VG_TIMESTEP_FIXED;
+   v3_muladds( player->rb.v, manifold->n, DOWNFORCE, player->rb.v );
+   m3x3_identity( s->state.velocity_bias );
+   m3x3_identity( s->state.velocity_bias_pstep );
+
+   if( s->state.activity_prev != k_skate_activity_grind )
+   {
+      /* FIXME: Queue audio events instead */
+#if 0
+      audio_lock();
+      audio_player_set_flags( &audio_player_extra, 
+                              AUDIO_FLAG_SPACIAL_3D );
+      audio_player_set_position( &audio_player_extra, player.rb.co );
+      audio_player_set_vol( &audio_player_extra, 20.0f );
+      audio_player_playclip( &audio_player_extra, &audio_board[5] );
+      audio_unlock();
+#endif
+   }
+}
+
+/*
+ * Air control, no real physics
+ */
+VG_STATIC void skate_apply_air_model( player_interface *player,
+                                      struct player_device_skate *s )
+{
+   if( s->state.activity != k_skate_activity_air )
+      return;
+
+   if( s->state.activity_prev != k_skate_activity_air )
+      player_approximate_best_trajectory( player, s );
+
+   m3x3_mulv( s->state.velocity_bias, player->rb.v, player->rb.v );
+   ray_hit hit;
+
+   /* 
+    * Prediction 
+    */
+   float pstep  = VG_TIMESTEP_FIXED * 1.0f;
+   float k_bias = 0.98f;
+
+   v3f pco, pco1, pv;
+   v3_copy( player->rb.co, pco );
+   v3_muls( player->rb.v, 1.0f, pv );
+   
+   float time_to_impact = 0.0f;
+   float limiter = 1.0f;
+
+   struct grind_edge *best_grind = NULL;
+   float closest_grind = INFINITY;
+
+   v3f target_normal = { 0.0f, 1.0f, 0.0f };
+   int has_target = 0;
+
+   for( int i=0; i<250; i++ )
+   {
+      v3_copy( pco, pco1 );
+      m3x3_mulv( s->state.velocity_bias, pv, pv );
+
+      pv[1] += -k_gravity * pstep;
+      v3_muladds( pco, pv, pstep, pco );
+      
+      ray_hit contact;
+      v3f vdir;
+
+      v3_sub( pco, pco1, vdir );
+      contact.dist = v3_length( vdir );
+      v3_divs( vdir, contact.dist, vdir);
+
+      v3f c0, c1;
+      struct grind_edge *ge = skate_collect_grind_edge( pco, pco1,
+                                                        c0, c1, 0.4f );
+
+      if( ge && (v3_dot((v3f){0.0f,1.0f,0.0f},vdir) < -0.2f ) )
+      {
+         vg_line( ge->p0, ge->p1, 0xff0000ff );
+         vg_line_cross( pco, 0xff0000ff, 0.25f );
+         has_target = 1;
+         break;
+      }
+      
+      float orig_dist = contact.dist;
+      if( ray_world( pco1, vdir, &contact ) )
+      {
+         v3_copy( contact.normal, target_normal );
+         has_target = 1;
+         time_to_impact += (contact.dist/orig_dist)*pstep;
+         vg_line_cross( contact.pos, 0xffff0000, 0.25f );
+         break;
+      }
+      time_to_impact += pstep;
+   }
+
+   if( has_target )
+   {
+      float angle = v3_dot( player->rb.to_world[1], target_normal );
+      v3f axis; 
+      v3_cross( player->rb.to_world[1], target_normal, axis );
+
+      limiter = vg_minf( 5.0f, time_to_impact )/5.0f;
+      limiter = 1.0f-limiter;
+      limiter *= limiter;
+      limiter = 1.0f-limiter;
+
+      if( fabsf(angle) < 0.99f )
+      {
+         v4f correction;
+         q_axis_angle( correction, axis, 
+                        acosf(angle)*(1.0f-limiter)*3.0f*VG_TIMESTEP_FIXED );
+         q_mul( correction, player->rb.q, player->rb.q );
+      }
+   }
+
+   v2f steer = { player->input_js1h->axis.value,
+                 player->input_js1v->axis.value };
+   v2_normalize_clamp( steer );
+
+   s->state.steery -= steer[0] * k_steer_air * VG_TIMESTEP_FIXED;
+   s->state.steerx += steer[1] * s->state.reverse * k_steer_air 
+                                                         * limiter * k_rb_delta;
+}
+
+VG_STATIC void skate_get_board_points( player_interface *player,
+                                       struct player_device_skate *s,
+                                       v3f front, v3f back )
+{
+   v3f pos_front = {0.0f,0.0f,-k_board_length},
+       pos_back  = {0.0f,0.0f, k_board_length};
+
+   m4x3_mulv( player->rb.to_world, pos_front, front );
+   m4x3_mulv( player->rb.to_world, pos_back,  back );
+}
+
+/*
+ * Casts and pushes a sphere-spring model into the world
+ */
+VG_STATIC int skate_simulate_spring( player_interface *player,
+                                     struct player_device_skate *s,
+                                     v3f pos )
+{
+   float mod      = 0.7f * player->input_grab->axis.value + 0.3f,
+         spring_k = mod * k_spring_force,
+         damp_k   = mod * k_spring_dampener,
+         disp_k   = 0.4f;
+
+   v3f start, end;
+   v3_copy( pos, start );
+   v3_muladds( pos, player->rb.to_world[1], -disp_k, end );
+
+   float t;
+   v3f n;
+   int hit_info = spherecast_world( start, end, 0.2f, &t, n );
+
+   if( hit_info != -1 )
+   {
+      v3f F, delta;
+      v3_sub( start, player->rb.co, delta );
+      
+      float displacement = vg_clampf( 1.0f-t, 0.0f, 1.0f ),
+            damp         = 
+         vg_maxf( 0.0f, v3_dot( player->rb.to_world[1], player->rb.v ) );
+
+      v3_muls( player->rb.to_world[1], displacement*spring_k*k_rb_delta -
+                                       damp*damp_k*k_rb_delta, F );
+
+      v3_muladds( player->rb.v, F, 1.0f, player->rb.v );
+      
+      /* Angular velocity */
+      v3f wa;
+      v3_cross( delta, F, wa );
+      v3_muladds( player->rb.w, wa, k_spring_angular, player->rb.w );
+
+      v3_lerp( start, end, t, pos );
+      return 1;
+   }
+   else
+   {
+      v3_copy( end, pos );
+      return 0;
+   }
+}
+
+
+/* 
+ * Handles connection between the player and the ground
+ */
+VG_STATIC void skate_apply_interface_model( player_interface *player,
+                                            struct player_device_skate *s,
+                                            rb_ct *manifold, int len )
+{
+   if( !((s->state.activity == k_skate_activity_ground) ||
+         (s->state.activity == k_skate_activity_air )) )
+      return;
+
+   if( s->state.activity == k_skate_activity_air )
+      s->debug_normal_pressure = 0.0f;
+   else
+      s->debug_normal_pressure = v3_dot( player->rb.to_world[1], player->rb.v );
+
+   /* springs */
+   v3f spring0, spring1;
+
+   skate_get_board_points( player, s, spring1, spring0 );
+   int spring_hit0 = skate_simulate_spring( player, s, spring0 ),
+       spring_hit1 = skate_simulate_spring( player, s, spring1 );
+
+   v3f animavg, animdelta;
+   v3_add( spring0, spring1, animavg );
+   v3_muls( animavg, 0.5f, animavg );
+
+   v3_sub( spring1, spring0, animdelta );
+   v3_normalize( animdelta );
+
+   m4x3_mulv( player->rb.to_local, animavg, s->board_offset );
+
+   float dx = -v3_dot( animdelta, player->rb.to_world[2] ),
+         dy =  v3_dot( animdelta, player->rb.to_world[1] );
+
+   float angle = -atan2f( dy, dx );
+   q_axis_angle( s->board_rotation, (v3f){1.0f,0.0f,0.0f}, angle );
+
+   /* Surface connection */
+   if( len == 0 && !(spring_hit0 && spring_hit1) )
+   {
+      s->state.lift_frames ++;
+
+      if( s->state.lift_frames >= 8 )
+         s->state.activity = k_skate_activity_air;
+   }
+   else
+   {
+      v3f surface_avg;
+      v3_zero( surface_avg );
+
+      for( int i=0; i<len; i++ )
+         v3_add( surface_avg, manifold[i].n, surface_avg );
+      v3_normalize( surface_avg );
+      
+      if( v3_dot( player->rb.v, surface_avg ) > 0.7f )
+      {
+         s->state.lift_frames ++;
+
+         if( s->state.lift_frames >= 8 )
+            s->state.activity = k_skate_activity_air;
+      }
+      else
+      {
+         s->state.activity = k_skate_activity_ground;
+         s->state.lift_frames = 0;
+         v3f projected, axis;
+
+         float const DOWNFORCE = -k_downforce*VG_TIMESTEP_FIXED;
+         v3_muladds( player->rb.v, player->rb.to_world[1], 
+                     DOWNFORCE, player->rb.v );
+
+         float d = v3_dot( player->rb.to_world[2], surface_avg );
+         v3_muladds( surface_avg, player->rb.to_world[2], -d, projected );
+         v3_normalize( projected );
+
+         float angle = v3_dot( player->rb.to_world[1], projected );
+         v3_cross( player->rb.to_world[1], projected, axis );
+
+         if( fabsf(angle) < 0.9999f )
+         {
+            v4f correction;
+            q_axis_angle( correction, axis, 
+                          acosf(angle)*4.0f*VG_TIMESTEP_FIXED );
+            q_mul( correction, player->rb.q, player->rb.q );
+         }
+      }
+   }
+}
+
+VG_STATIC void skate_apply_grab_model( player_interface *player,
+                                       struct player_device_skate *s )
+{
+   float grabt = player->input_grab->axis.value;
+
+   if( grabt > 0.5f )
+   {
+      v2_muladds( s->state.grab_mouse_delta, vg.mouse_delta, 0.02f, 
+                  s->state.grab_mouse_delta );
+
+      v2_normalize_clamp( s->state.grab_mouse_delta );
+   }
+   else
+      v2_zero( s->state.grab_mouse_delta );
+
+   s->state.grabbing = vg_lerpf( s->state.grabbing, grabt, 8.4f*k_rb_delta );
+}
+
+/*
+ * Computes friction and surface interface model
+ */
+VG_STATIC void skate_apply_friction_model( player_interface *player,
+                                           struct player_device_skate *s )
+{
+   if( s->state.activity != k_skate_activity_ground )
+      return;
+
+   /*
+    * Computing localized friction forces for controlling the character
+    * Friction across X is significantly more than Z
+    */
+
+   v3f vel;
+   m3x3_mulv( player->rb.to_local, player->rb.v, vel );
+   float slip = 0.0f;
+   
+   if( fabsf(vel[2]) > 0.01f )
+      slip = fabsf(-vel[0] / vel[2]) * vg_signf(vel[0]);
+
+   if( fabsf( slip ) > 1.2f )
+      slip = vg_signf( slip ) * 1.2f;
+
+   s->state.slip = slip;
+   s->state.reverse = -vg_signf(vel[2]);
+
+   vel[0] += vg_cfrictf( vel[0], k_friction_lat * k_rb_delta );
+   vel[2] += vg_cfrictf( vel[2], k_friction_resistance * k_rb_delta );
+
+   /* Pushing additive force */
+
+   if( !player->input_jump->button.value )
+   {
+      if( player->input_push->button.value )
+      {
+         if( (vg.time - s->state.cur_push) > 0.25 )
+            s->state.start_push = vg.time;
+
+         s->state.cur_push = vg.time;
+
+         double push_time = vg.time - s->state.start_push;
+
+         float cycle_time = push_time*k_push_cycle_rate,
+               accel      = k_push_accel * (sinf(cycle_time)*0.5f+0.5f),
+               amt        = accel * VG_TIMESTEP_FIXED,
+               current    = v3_length( vel ),
+               new_vel    = vg_minf( current + amt, k_max_push_speed ),
+               delta      = new_vel - vg_minf( current, k_max_push_speed );
+
+         vel[2] += delta * -s->state.reverse;
+      }
+   }
+
+   /* Send back to velocity */
+   m3x3_mulv( player->rb.to_world, vel, player->rb.v );
+   
+   /* Steering */
+   float input = player->input_js1h->axis.value,
+         grab  = player->input_grab->axis.value,
+         steer = input * (1.0f-(s->state.jump_charge+grab)*0.4f),
+         steer_scaled = vg_signf(steer) * powf(steer,2.0f) * k_steer_ground;
+
+   s->state.steery -= steer_scaled * k_rb_delta;
+}
+
+VG_STATIC void skate_apply_jump_model( player_interface *player,
+                                       struct player_device_skate *s )
+{
+   int charging_jump_prev = s->state.charging_jump;
+   s->state.charging_jump = player->input_jump->button.value;
+
+   /* Cannot charge this in air */
+   if( s->state.activity != k_skate_activity_ground )
+      s->state.charging_jump = 0;
+
+   if( s->state.charging_jump )
+   {
+      s->state.jump_charge += k_rb_delta * k_jump_charge_speed;
+
+      if( !charging_jump_prev )
+         s->state.jump_dir = s->state.reverse>0.0f? 1: 0;
+   }
+   else
+   {
+      s->state.jump_charge -= k_jump_charge_speed * VG_TIMESTEP_FIXED;
+   }
+
+   s->state.jump_charge = vg_clampf( s->state.jump_charge, 0.0f, 1.0f );
+
+   if( s->state.activity == k_skate_activity_air )
+      return;
+
+   /* player let go after charging past 0.2: trigger jump */
+   if( (!s->state.charging_jump) && (s->state.jump_charge > 0.2f) )
+   {
+      v3f jumpdir;
+      
+      /* Launch more up if alignment is up else improve velocity */
+      float aup = v3_dot( (v3f){0.0f,1.0f,0.0f}, player->rb.to_world[1] ),
+            mod = 0.5f,
+            dir = mod + fabsf(aup)*(1.0f-mod);
+
+      v3_copy( player->rb.v, jumpdir );
+      v3_normalize( jumpdir );
+      v3_muls( jumpdir, 1.0f-dir, jumpdir );
+      v3_muladds( jumpdir, player->rb.to_world[1], dir, jumpdir );
+      v3_normalize( jumpdir );
+      
+      float force = k_jump_force*s->state.jump_charge;
+      v3_muladds( player->rb.v, jumpdir, force, player->rb.v );
+      s->state.jump_charge = 0.0f;
+
+      s->state.jump_time = vg.time;
+      
+      /* FIXME audio events */
+#if 0
+      audio_lock();
+      audio_player_set_flags( &audio_player_extra, AUDIO_FLAG_SPACIAL_3D );
+      audio_player_set_position( &audio_player_extra, player.rb.co );
+      audio_player_set_vol( &audio_player_extra, 20.0f );
+      audio_player_playclip( &audio_player_extra, &audio_jumps[rand()%2] );
+      audio_unlock();
+#endif
+   }
+}
+
+VG_STATIC void skate_apply_pump_model( player_interface *player,
+                                       struct player_device_skate *s )
+{
+   /* Throw / collect routine 
+    *
+    * TODO: Max speed boost
+    */
+   if( player->input_grab->axis.value > 0.5f )
+   {
+      if( s->state.activity == k_skate_activity_ground )
+      {
+         /* Throw */
+         v3_muls( player->rb.to_world[1], k_mmthrow_scale, s->state.throw_v );
+      }
+   }
+   else
+   {
+      /* Collect */
+      float doty = v3_dot( player->rb.to_world[1], s->state.throw_v );
+      
+      v3f Fl, Fv;
+      v3_muladds( s->state.throw_v, player->rb.to_world[1], -doty, Fl);
+
+      if( s->state.activity == k_skate_activity_ground )
+      {
+         v3_muladds( player->rb.v,     Fl,  k_mmcollect_lat, player->rb.v );
+         v3_muladds( s->state.throw_v, Fl, -k_mmcollect_lat, s->state.throw_v );
+      }
+
+      v3_muls( player->rb.to_world[1], -doty, Fv );
+      v3_muladds( player->rb.v,     Fv, k_mmcollect_vert, player->rb.v );
+      v3_muladds( s->state.throw_v, Fv, k_mmcollect_vert, s->state.throw_v );
+   }
+
+   /* Decay */
+   if( v3_length2( s->state.throw_v ) > 0.0001f )
+   {
+      v3f dir;
+      v3_copy( s->state.throw_v, dir );
+      v3_normalize( dir );
+
+      float max = v3_dot( dir, s->state.throw_v ),
+            amt = vg_minf( k_mmdecay * k_rb_delta, max );
+      v3_muladds( s->state.throw_v, dir, -amt, s->state.throw_v );
+   }
+}
+
+VG_STATIC void skate_apply_cog_model( player_interface *player,
+                                      struct player_device_skate *s )
+{
+   v3f ideal_cog, ideal_diff;
+   v3_muladds( player->rb.co, player->rb.to_world[1],
+               1.0f-player->input_grab->axis.value, ideal_cog );
+   v3_sub( ideal_cog, s->state.cog, ideal_diff );
+
+   /* Apply velocities */
+   v3f rv;
+   v3_sub( player->rb.v, s->state.cog_v, rv );
+
+   v3f F;
+   v3_muls( ideal_diff, -k_cog_spring * k_rb_rate, F );
+   v3_muladds( F, rv,   -k_cog_damp * k_rb_rate, F );
+
+   float ra = k_cog_mass_ratio,
+         rb = 1.0f-k_cog_mass_ratio;
+
+   /* Apply forces & intergrate */
+   v3_muladds( s->state.cog_v, F, -rb, s->state.cog_v );
+   s->state.cog_v[1] += -9.8f * k_rb_delta;
+   v3_muladds( s->state.cog, s->state.cog_v, k_rb_delta, s->state.cog );
+}
+
+VG_STATIC void skate_collision_response( player_interface *player,
+                                         struct player_device_skate *s,
+                                         rb_ct *manifold, int len )
+{
+   for( int j=0; j<10; j++ )
+   {
+      for( int i=0; i<len; i++ )
+      {
+         struct contact *ct = &manifold[i];
+         
+         v3f dv, delta;
+         v3_sub( ct->co, player->rb.co, delta ); 
+         v3_cross( player->rb.w, delta, dv );
+         v3_add( player->rb.v, dv, dv );
+
+         float vn = -v3_dot( dv, ct->n );
+         vn += ct->bias;
+
+         float temp = ct->norm_impulse;
+         ct->norm_impulse = vg_maxf( temp + vn, 0.0f );
+         vn = ct->norm_impulse - temp;
+
+         v3f impulse;
+         v3_muls( ct->n, vn, impulse );
+
+         if( fabsf(v3_dot( impulse, player->rb.to_world[2] )) > 10.0f ||
+             fabsf(v3_dot( impulse, player->rb.to_world[1] )) > 50.0f )
+         {
+            /* FIXME */
+#if 0
+            player_kill();
+            return;
+#endif
+         }
+
+         v3_add( impulse, player->rb.v, player->rb.v );
+         v3_cross( delta, impulse, impulse );
+
+         /*
+          * W Impulses are limited to the Y and X axises, we don't really want
+          * roll angular velocities being included.
+          *
+          * Can also tweak the resistance of each axis here by scaling the wx,wy
+          * components.
+          */
+         
+         float wy = v3_dot( player->rb.to_world[1], impulse ) * 0.8f,
+               wx = v3_dot( player->rb.to_world[0], impulse ) * 1.0f;
+
+         v3_muladds( player->rb.w, player->rb.to_world[1], wy, player->rb.w );
+         v3_muladds( player->rb.w, player->rb.to_world[0], wx, player->rb.w );
+      }
+   }
+}
+
+VG_STATIC void skate_integrate( player_interface *player,
+                                struct player_device_skate *s )
+{
+   /* integrate rigidbody velocities */
+   v3f gravity = { 0.0f, -9.6f, 0.0f };
+   v3_muladds( player->rb.v, gravity, k_rb_delta, player->rb.v );
+   v3_muladds( player->rb.co, player->rb.v, k_rb_delta, player->rb.co );
+
+   v3_lerp( player->rb.w, (v3f){0.0f,0.0f,0.0f}, 0.125f*0.5f, player->rb.w );
+   if( v3_length2( player->rb.w ) > 0.0f )
+   {
+      v4f rotation;
+      v3f axis;
+      v3_copy( player->rb.w, axis );
+      
+      float mag = v3_length( axis );
+      v3_divs( axis, mag, axis );
+      q_axis_angle( rotation, axis, mag*k_rb_delta );
+      q_mul( rotation, player->rb.q, player->rb.q );
+   }
+
+   /* integrate steering velocities */
+   v4f rotate; 
+   float l = (s->state.activity == k_skate_activity_air)? 0.04f: 0.3f;
+
+   s->state.steery_s = vg_lerpf( s->state.steery_s, s->state.steery, l );
+   s->state.steerx_s = vg_lerpf( s->state.steerx_s, s->state.steerx, l );
+
+   q_axis_angle( rotate, player->rb.to_world[1], s->state.steery_s );
+   q_mul( rotate, player->rb.q, player->rb.q );
+
+   q_axis_angle( rotate, player->rb.to_world[0], s->state.steerx_s );
+   q_mul( rotate, player->rb.q, player->rb.q );
+
+   s->state.steerx = 0.0f;
+   s->state.steery = 0.0f;
+
+#if 0
+   v3_sub( player.rb.v, s->phys.v_prev, s->phys.a );
+   v3_muls( s->phys.a, 1.0f/VG_TIMESTEP_FIXED, s->phys.a );
+   v3_copy( player.rb.v, s->phys.v_prev );
+#endif
+
+   rb_update_transform( &player->rb );
+}
+
+VG_STATIC void player_skate_update( player_interface *player,
+                                    player_attachment *at )
+{
+   struct player_device_skate *s = at->storage;
+   s->state.activity_prev = s->state.activity;
+
+   /* Setup colliders */
+   m4x3f mtx_front, mtx_back;
+   m3x3_identity( mtx_front );
+   m3x3_identity( mtx_back );
+
+   skate_get_board_points( player, s, mtx_front[3], mtx_back[3] );
+
+   s->sphere_back.radius = 0.3f;
+   s->sphere_front.radius = 0.3f;
+
+   /* create manifold(s) */
+   rb_ct manifold[72],
+         *interface_manifold = NULL,
+         *grind_manifold = NULL;
+
+   int 
+   len_front = skate_collide_smooth( player, mtx_front, 
+                                     &s->sphere_front, manifold ),
+   len_back = skate_collide_smooth( player, mtx_back,  
+                                    &s->sphere_back, &manifold[len_front] ),
+
+   interface_len = len_front + len_back;
+
+   interface_manifold = manifold;
+   grind_manifold = manifold + interface_len;
+
+   int grind_len = skate_grind_collide( player, at, grind_manifold );
+
+   for( int i=0; i<interface_len+grind_len; i ++ )
+   {
+      rb_prepare_contact( &manifold[i] );
+      rb_debug_contact( &manifold[i] );
+   }
+
+   skate_apply_grind_model( player, s, grind_manifold, grind_len );
+   skate_apply_interface_model( player, s, manifold, interface_len );
+   
+   skate_apply_pump_model( player, s );
+   skate_apply_cog_model( player, s );
+   skate_collision_response( player, s, manifold, interface_len + grind_len );
+
+   skate_apply_grab_model( player, s );
+   skate_apply_friction_model( player, s );
+   skate_apply_jump_model( player, s );
+   skate_apply_air_model( player, s );
+
+   skate_integrate( player, s );
+}
+
+VG_STATIC void player_skate_post_update( player_interface *player,
+                                         player_attachment *at )
+{
+}
+
+VG_STATIC void player_skate_ui( player_interface *player,
+                                player_attachment *at )
+{
+   struct player_device_skate *s = at->storage;
+
+   /* FIXME: Compression */
+   player_debugtext( 1, "V:  %5.2f %5.2f %5.2f",player->rb.v[0],
+                                                player->rb.v[1],
+                                                player->rb.v[2] );
+   player_debugtext( 1, "CO: %5.2f %5.2f %5.2f",player->rb.co[0],
+                                                player->rb.co[1],
+                                                player->rb.co[2] );
+   player_debugtext( 1, "W:  %5.2f %5.2f %5.2f",player->rb.w[0],
+                                                player->rb.w[1],
+                                                player->rb.w[2] );
+
+   player_debugtext( 1, "activity: %s\n",
+                           (const char *[]){ "k_skate_activity_air",
+                                             "k_skate_activity_ground",
+                                             "k_skate_activity_grind }" }
+                                             [s->state.activity] );
+}
+
+VG_STATIC void player_skate_pose( player_interface *player,
+                                  player_attachment *at, 
+                                  player_pose pose, m4x3f transform )
+{
+   struct player_device_skate *s = at->storage;
+   struct player_avatar *av = player->playeravatar;
+   struct skeleton *sk = &av->sk;
+
+   /* Camera position */
+   /* TODO split up */
+   /* FIXME */
+
+#if 0
+   v3_muladds( phys->m, phys->a, VG_TIMESTEP_FIXED, phys->m );
+   v3_lerp( phys->m, (v3f){0.0f,0.0f,0.0f}, 0.1f, phys->m );
+
+   phys->m[0] = vg_clampf( phys->m[0], -2.0f, 2.0f );
+   phys->m[1] = vg_clampf( phys->m[1], -2.0f, 2.0f );
+   phys->m[2] = vg_clampf( phys->m[2], -2.0f, 2.0f );
+   v3_lerp( phys->bob, phys->m, 0.2f, phys->bob );
+#endif
+
+   /* Head */
+   float kheight = 2.0f,
+         kleg = 0.6f;
+
+   v3f offset;
+   v3_zero( offset );
+
+#if 0
+   m3x3_mulv( player.inv_visual_transform, phys->bob, offset );
+#endif
+
+   static float speed_wobble = 0.0f, speed_wobble_2 = 0.0f;
+
+   float curspeed  = v3_length( player->rb.v ),
+         kickspeed = vg_clampf( curspeed*(1.0f/40.0f), 0.0f, 1.0f ),
+         kicks     = (vg_randf()-0.5f)*2.0f*kickspeed,
+         sign      = vg_signf( kicks );
+
+   s->wobble[0] = vg_lerpf( s->wobble[0], kicks*kicks*sign, 6.0f*vg.time_delta);
+   s->wobble[1] = vg_lerpf( s->wobble[1], speed_wobble,     2.4f*vg.time_delta);
+
+   offset[0] *= 0.26f;
+   offset[0] += speed_wobble_2*3.0f;
+
+   offset[1] *= -0.3f;
+   offset[2] *= 0.01f;
+
+   offset[0]=vg_clampf(offset[0],-0.8f,0.8f)*(1.0f-fabsf(s->blend_slide)*0.9f);
+   offset[1]=vg_clampf(offset[1],-0.5f,0.0f);
+
+   /* 
+    * Animation blending
+    * ===========================================
+    */
+   
+   /* sliding */
+   {
+      float desired  = vg_clampf( fabsf( s->state.slip ), 0.0f, 1.0f );
+      s->blend_slide = vg_lerpf( s->blend_slide, desired, 2.4f*vg.time_delta);
+   }
+   
+   /* movement information */
+   {
+      int iair = (s->state.activity == k_skate_activity_air) ||
+                 (s->state.activity == k_skate_activity_grind );
+
+      float dirz = s->state.reverse > 0.0f? 0.0f: 1.0f,
+            dirx = s->state.slip < 0.0f?    0.0f: 1.0f,
+            fly  = iair?                    1.0f: 0.0f;
+
+      s->blend_z    = vg_lerpf( s->blend_z,   dirz, 2.4f*vg.time_delta );
+      s->blend_x    = vg_lerpf( s->blend_x,   dirx, 0.6f*vg.time_delta );
+      s->blend_fly  = vg_lerpf( s->blend_fly, fly,  2.4f*vg.time_delta );
+   }
+
+   mdl_keyframe apose[32], bpose[32];
+   mdl_keyframe ground_pose[32];
+   {
+      /* when the player is moving fast he will crouch down a little bit */
+      float stand = 1.0f - vg_clampf( curspeed * 0.03f, 0.0f, 1.0f );
+      s->blend_stand = vg_lerpf( s->blend_stand, stand, 6.0f*vg.time_delta );
+
+      /* stand/crouch */
+      float dir_frame   = s->blend_z * (15.0f/30.0f),
+            stand_blend = offset[1]*-2.0f;
+
+      v3f local_cog;
+      m4x3_mulv( player->rb.to_local, s->state.cog, local_cog );
+
+      stand_blend = vg_clampf( 1.0f-local_cog[1], 0, 1 );
+
+      skeleton_sample_anim( sk, s->anim_stand, dir_frame, apose );
+      skeleton_sample_anim( sk, s->anim_highg, dir_frame, bpose );
+      skeleton_lerp_pose( sk, apose, bpose, stand_blend, apose );
+
+      /* sliding */
+      float slide_frame = s->blend_x * (15.0f/30.0f);
+      skeleton_sample_anim( sk, s->anim_slide, slide_frame, bpose );
+      skeleton_lerp_pose( sk, apose, bpose, s->blend_slide, apose );
+
+      /* pushing */
+      double push_time = vg.time - s->state.start_push;
+      s->blend_push = vg_lerpf( s->blend_push,
+                               (vg.time - s->state.cur_push) < 0.125,
+                               6.0f*vg.time_delta );
+
+      float pt = push_time + vg.accumulator;
+      if( s->state.reverse > 0.0f )
+         skeleton_sample_anim( sk, s->anim_push, pt, bpose );
+      else
+         skeleton_sample_anim( sk, s->anim_push_reverse, pt, bpose );
+
+      skeleton_lerp_pose( sk, apose, bpose, s->blend_push, apose );
+
+      /* trick setup */
+      float jump_start_frame = 14.0f/30.0f;
+
+      float charge = s->state.jump_charge;
+      s->blend_jump = vg_lerpf( s->blend_jump, charge, 8.4f*vg.time_delta );
+
+      float setup_frame = charge * jump_start_frame,
+            setup_blend = vg_minf( s->blend_jump, 1.0f );
+      
+      float jump_frame = (vg.time - s->state.jump_time) + jump_start_frame;
+      if( jump_frame >= jump_start_frame && jump_frame <= (40.0f/30.0f) )
+         setup_frame = jump_frame;
+
+      struct skeleton_anim *jump_anim = s->state.jump_dir?
+                                        s->anim_ollie:
+                                        s->anim_ollie_reverse;
+
+      skeleton_sample_anim_clamped( sk, jump_anim, setup_frame, bpose );
+      skeleton_lerp_pose( sk, apose, bpose, setup_blend, ground_pose );
+   }
+   
+   mdl_keyframe air_pose[32];
+   {
+      float target = -player->input_js1h->axis.value;
+      s->blend_airdir = vg_lerpf( s->blend_airdir, target, 2.4f*vg.time_delta );
+      
+      float air_frame = (s->blend_airdir*0.5f+0.5f) * (15.0f/30.0f);
+      skeleton_sample_anim( sk, s->anim_air, air_frame, apose );
+
+      static v2f grab_choice;
+
+      v2f grab_input = { player->input_js2h->axis.value,
+                         player->input_js2v->axis.value };
+      v2_add( s->state.grab_mouse_delta, grab_input, grab_input );
+      if( v2_length2( grab_input ) <= 0.001f )
+         grab_input[0] = -1.0f;
+      else
+         v2_normalize_clamp( grab_input );
+      v2_lerp( grab_choice, grab_input, 2.4f*vg.time_delta, grab_choice );
+
+      float ang = atan2f( grab_choice[0], grab_choice[1] ),
+            ang_unit = (ang+VG_PIf) * (1.0f/VG_TAUf),
+            grab_frame = ang_unit * (15.0f/30.0f);
+
+      skeleton_sample_anim( sk, s->anim_grabs, grab_frame, bpose );
+      skeleton_lerp_pose( sk, apose, bpose, s->state.grabbing, air_pose );
+   }
+
+   skeleton_lerp_pose( sk, ground_pose, air_pose, s->blend_fly, pose );
+
+   float add_grab_mod = 1.0f - s->blend_fly;
+
+   /* additive effects */
+   {
+      u32 apply_to[] = { av->id_hip, 
+                         av->id_ik_hand_l,
+                         av->id_ik_hand_r,
+                         av->id_ik_elbow_l,
+                         av->id_ik_elbow_r };
+
+      for( int i=0; i<vg_list_size(apply_to); i ++ )
+      {
+         pose[apply_to[i]-1].co[0] += offset[0]*add_grab_mod;
+         pose[apply_to[i]-1].co[2] += offset[2]*add_grab_mod;
+      }
+
+      mdl_keyframe *kf_board  = &pose[av->id_board-1],
+                   *kf_foot_l = &pose[av->id_ik_foot_l-1],
+                   *kf_foot_r = &pose[av->id_ik_foot_r-1];
+
+      v3f bo;
+      v3_muls( s->board_offset, add_grab_mod, bo );
+
+      v3_add( bo, kf_board->co,  kf_board->co );
+      v3_add( bo, kf_foot_l->co, kf_foot_l->co );
+      v3_add( bo, kf_foot_r->co, kf_foot_r->co );
+
+      m3x3f c;
+      q_m3x3( s->board_rotation, c );
+
+      v3f d;
+      v3_sub( kf_foot_l->co, bo, d );
+      m3x3_mulv( c, d, d );
+      v3_add( bo, d, kf_foot_l->co );
+      
+      v3_sub( kf_foot_r->co, bo, d );
+      m3x3_mulv( c, d, d );
+      v3_add( bo, d, kf_foot_r->co );
+
+      q_mul( s->board_rotation, kf_board->q, kf_board->q );
+      q_normalize( kf_board->q );
+   }
+
+   /* transform */
+   rb_extrapolate_transform( &player->rb, transform );
+
+#if 0
+   v3_muladds( player.visual_transform[3], phys->rb.up, -0.2f, 
+               player.visual_transform[3] );
+#endif
+
+   v4f qresy, qresx, qresidual;
+   m3x3f mtx_residual;
+   float substep = vg_clampf( vg.accumulator / VG_TIMESTEP_FIXED, 0.0f, 1.0f );
+   q_axis_angle( qresy, player->rb.to_world[1], s->state.steery_s*substep );
+   q_axis_angle( qresx, player->rb.to_world[0], s->state.steerx_s*substep );
+
+   q_mul( qresy, qresx, qresidual );
+   q_m3x3( qresidual, mtx_residual );
+   m3x3_mul( transform, mtx_residual, transform );
+}
+
+VG_STATIC void player_skate_get_camera( player_interface *player,
+                                        player_attachment *at, camera *cam )
+{
+   struct player_device_skate *s = at->storage;
+   struct player_avatar *av = player->playeravatar;
+
+   /* FIXME: viewpoint entity */
+   v3f vp = {-0.1f,1.8f,0.0f};
+   m4x3_mulv( av->sk.final_mtx[ av->id_head-1 ], vp, cam->pos );
+
+   v3_zero( cam->angles );
+   cam->fov = 119.0f;
+
+   /* TODO: smooth clamp lerp rate of change */
+
+   v3_lerp( s->state.vl, player->rb.v, 5.0f*vg.time_delta, s->state.vl );
+
+   float *v  = s->state.vl,
+         yaw = atan2f( v[0], -v[2] ),
+       pitch = atan2f
+               ( 
+                   -v[1], 
+                   sqrtf
+                   (
+                     v[0]*v[0] + v[2]*v[2]
+                   )
+               ) 
+               * 0.7f + 0.5f;
+
+   cam->angles[0] = yaw;
+   cam->angles[1] = pitch;
+}
+
+VG_STATIC void player_skate_transport( player_interface *player,
+                                       player_attachment *at,
+                                       teleport_gate *gate )
+{
+   struct player_device_skate *s = at->storage;
+
+   m4x3_mulv( gate->transport, player->rb.co,  player->rb.co );
+   m4x3_mulv( gate->transport, s->state.cog,   s->state.cog );
+   m3x3_mulv( gate->transport, s->state.cog_v, s->state.cog_v );
+   m3x3_mulv( gate->transport, player->rb.v,   player->rb.v );
+   m3x3_mulv( gate->transport, s->state.vl,    s->state.vl );
+
+   v4f transport_rotation;
+   m3x3_q( gate->transport, transport_rotation );
+   q_mul( transport_rotation, player->rb.q, player->rb.q );
+
+   s->state_gate_storage = s->state;
+}
+
+VG_STATIC player_device player_device_skate =
+{
+   .pre_update    = player_skate_pre_update,
+   .update        = player_skate_update,
+   .post_update   = player_skate_post_update,
+   .get_camera    = player_skate_get_camera,
+   .debug_ui      = player_skate_ui,
+   .bind          = player_skate_bind,
+   .pose          = player_skate_pose,
+   .gate_transport= player_skate_transport
+};
+
+#endif /* PLAYER_DEVICE_SKATE_H */
index a0ea96bd03a7af00944ac7e95e005cbc73f8e79d..508feef399ffe1e7b39fccc0528adf1642e69391 100644 (file)
@@ -30,7 +30,8 @@ struct player_device_walk
       }
       activity;
    }
-   state;
+   state,
+   state_gate_storage;
 
    enum mdl_surface_prop surface;
 
@@ -414,6 +415,25 @@ VG_STATIC void player_walk_get_camera( player_interface *player,
    cam->fov = 90.0f;
 }
 
+VG_STATIC void player_walk_transport( player_interface *player,
+                                      player_attachment *at,
+                                      teleport_gate *gate )
+{
+   struct player_device_walk *w = at->storage;
+
+   m4x3_mulv( gate->transport, player->rb.co, player->rb.co );
+   m3x3_mulv( gate->transport, player->rb.v,  player->rb.v );
+
+   /* analytical rotation of yaw */
+   v3f fwd_dir = { cosf(w->state.angles[0]),
+                   0.0f,
+                   sinf(w->state.angles[0])};
+   m3x3_mulv( gate->transport, fwd_dir, fwd_dir );
+   w->state.angles[0] = atan2f( fwd_dir[2], fwd_dir[0] );
+
+   w->state_gate_storage = w->state;
+}
+
 VG_STATIC player_device player_device_walk =
 {
    .pre_update    = player_walk_pre_update,
@@ -422,7 +442,8 @@ VG_STATIC player_device player_device_walk =
    .get_camera    = player_walk_get_camera,
    .debug_ui      = player_walk_ui,
    .bind          = player_walk_bind,
-   .pose          = player_walk_pose
+   .pose          = player_walk_pose,
+   .gate_transport= player_walk_transport
 };
 
 #endif /* PLAYER_DEVICE_WALK_H */
index f28d7a82ac5b960fd4238cae9b0c72f442c5bd14..1b9e16bc6b59528681d277fc0caee1b1a1c43846 100644 (file)
@@ -38,6 +38,8 @@ struct player_interface
                         *input_reset,
                         *input_grab;
 
+   v3f prev_position;
+
    struct player_avatar  *playeravatar;
    glmesh                *playermesh;
    struct player_ragdoll  ragdoll;
@@ -67,6 +69,8 @@ struct player_device
    void (* store_state)( player_interface *player, player_attachment *at );
    void (* load_state) ( player_interface *player, player_attachment *at );
    void (* debug_ui)   ( player_interface *player, player_attachment *at );
+   void (* gate_transport)( player_interface *player, player_attachment *at,
+                            teleport_gate *gate );
 };
 
 VG_STATIC void player_interface_create_player( player_interface *inst )
@@ -162,6 +166,8 @@ VG_STATIC void player_pre_update( player_interface *player )
 {
    assert( player->dev.device );
 
+   v3_copy( player->rb.co, player->prev_position );
+
    if( player->dev.device->pre_update )
       player->dev.device->pre_update( player, &player->dev );
 }
@@ -174,12 +180,37 @@ VG_STATIC void player_update( player_interface *player )
       player->dev.device->update( player, &player->dev );
 }
 
-VG_STATIC void player_post_update( player_interface *player )
+VG_STATIC void player_post_update( player_interface *player, 
+                                   camera *main_camera )
 {
    assert( player->dev.device );
 
    if( player->dev.device->post_update )
       player->dev.device->post_update( player, &player->dev );
+
+   /* FIXME: only need to test against the visible gate....
+    *        OR... bvh */
+
+   for( int i=0; i<world.gate_count; i++ )
+   {
+      struct route_gate *rg = &world.gates[i];
+      teleport_gate *gate = &rg->gate;
+
+      if( gate_intersect( gate, player->rb.co, player->prev_position ) )
+      {
+         player->dev.device->gate_transport( player, &player->dev, gate );
+         v3_copy( player->rb.co, player->prev_position );
+
+         /* Pre-emptively edit the camera matrices so that the motion vectors 
+          * are correct */
+         m4x3f transport_i;
+         m4x4f transport_4;
+         m4x3_invert_affine( gate->transport, transport_i );
+         m4x3_expand( transport_i, transport_4 );
+         m4x4_mul( main_camera->mtx.pv, transport_4, main_camera->mtx.pv );
+         m4x4_mul( main_camera->mtx.v, transport_4, main_camera->mtx.v );
+      }
+   }
    
 #if 0
    camera_update_transform( &player->cam );
@@ -274,9 +305,11 @@ VG_STATIC void player_spawn( player_interface *player,
                              struct respawn_point *rp )
 {
    v3_copy( rp->co, player->rb.co );
+   v3_copy( rp->co, player->prev_position );
    v3_zero( player->rb.v );
    v3_zero( player->rb.w );
    q_identity( player->rb.q );
+   rb_update_transform( &player->rb );
 }
 
 /*
index 56d793c553fb29dea868d59b43e437f76ae8d7f6..73bad63d3c1c5a45d2040938a4c22115b8970035 100644 (file)
@@ -13,6 +13,7 @@ VG_STATIC void player_integrate(void);
 
 VG_STATIC int player_collide_sphere( rigidbody *rb, rb_ct *manifold )
 {
+#if 0
    int len = 0;
 
    len = rb_sphere_scene( rb, &world.rb_geo, manifold );
@@ -30,6 +31,9 @@ VG_STATIC int player_collide_sphere( rigidbody *rb, rb_ct *manifold )
       len = new_len;
 
    return len;
+#endif
+
+   return 0;
 }
 
 #if 0
index 5c497f21ada170c53a4d7850095b54beffcd025f..d2ce94922cf494335774e918681562f1a40aaab2 100644 (file)
@@ -169,6 +169,11 @@ struct rb_constr_swingtwist
    float tangent_mass, axis_mass;
 };
 
+struct rb_constr_spring
+{
+   int nothing;
+};
+
 /*
  * -----------------------------------------------------------------------------
  *                               Math Utils
@@ -213,7 +218,7 @@ VG_STATIC void rb_debug_contact( rb_ct *ct )
    {
       v3f p1;
       v3_muladds( ct->co, ct->n, 0.05f, p1 );
-      vg_line_pt3( ct->co, 0.0025f, 0xff0000ff );
+      vg_line_pt3( ct->co, 0.0125f, 0xff0000ff );
       vg_line( ct->co, p1, 0xffffffff );
    }
 }
@@ -1276,6 +1281,7 @@ VG_STATIC int rb_sphere_sphere( rigidbody *rba, rigidbody *rbb, rb_ct *buf )
 
 //#define RIGIDBODY_DYNAMIC_MESH_EDGES
 
+__attribute__ ((deprecated))
 VG_STATIC int rb_sphere_triangle( rigidbody *rba, rigidbody *rbb, 
                                   v3f tri[3], rb_ct *buf )
 {
@@ -1323,6 +1329,45 @@ VG_STATIC int rb_sphere_triangle( rigidbody *rba, rigidbody *rbb,
    return 0;
 }
 
+VG_STATIC int rb_sphere__triangle( m4x3f mtxA, rb_sphere *b,
+                                   v3f tri[3], rb_ct *buf )
+{
+   v3f delta, co;
+   enum contact_type type = closest_on_triangle_1( mtxA[3], tri, co );
+
+   v3_sub( mtxA[3], co, delta );
+
+   float d2 = v3_length2( delta ),
+          r = b->radius;
+
+   if( d2 < r*r )
+   {
+      rb_ct *ct = buf;
+
+      v3f ab, ac, tn;
+      v3_sub( tri[2], tri[0], ab );
+      v3_sub( tri[1], tri[0], ac );
+      v3_cross( ac, ab, tn );
+      v3_copy( tn, ct->n );
+
+      if( v3_length2( ct->n ) <= 0.00001f )
+      {
+         vg_error( "Zero area triangle!\n" );
+         return 0;
+      }
+
+      v3_normalize( ct->n );
+
+      float d = sqrtf(d2);
+
+      v3_copy( co, ct->co );
+      ct->type = type;
+      ct->p = r-d;
+      return 1;
+   }
+
+   return 0;
+}
 
 VG_STATIC void rb_debug_sharp_scene_edges( rigidbody *rbb, float sharp_ang,
                                            boxf box, u32 colour )
@@ -1418,6 +1463,50 @@ VG_STATIC void rb_debug_sharp_scene_edges( rigidbody *rbb, float sharp_ang,
    }
 }
 
+VG_STATIC int rb_sphere__scene( m4x3f mtxA, rb_sphere *b,
+                                m4x3f mtxB, rb_scene *s, rb_ct *buf )
+{
+   scene *sc = s->bh_scene->user;
+
+   bh_iter it;
+   bh_iter_init( 0, &it );
+   int idx;
+
+   int count = 0;
+
+   float r = b->radius;
+   boxf box;
+   v3_sub( mtxA[3], (v3f){ r,r,r }, box[0] );
+   v3_add( mtxA[3], (v3f){ r,r,r }, box[1] );
+   
+   while( bh_next( s->bh_scene, &it, box, &idx ) )
+   {
+      u32 *ptri = &sc->arrindices[ idx*3 ];
+      v3f tri[3];
+
+      for( int j=0; j<3; j++ )
+         v3_copy( sc->arrvertices[ptri[j]].co, tri[j] );
+      
+      buf[ count ].element_id = ptri[0];
+
+      vg_line( tri[0],tri[1],0x70ff6000 );
+      vg_line( tri[1],tri[2],0x70ff6000 );
+      vg_line( tri[2],tri[0],0x70ff6000 );
+
+      int contact = rb_sphere__triangle( mtxA, b, tri, &buf[count] );
+      count += contact;
+
+      if( count == 16 )
+      {
+         vg_warn( "Exceeding sphere_vs_scene capacity. Geometry too dense!\n" );
+         return count;
+      }
+   }
+
+   return count;
+}
+
+__attribute__ ((deprecated))
 VG_STATIC int rb_sphere_scene( rigidbody *rba, rigidbody *rbb, rb_ct *buf )
 {
    scene *sc = rbb->inf.scene.bh_scene->user;
index be5d4d699f3cc5859eac179fb6c5bbc3e529fed3..7420fbda9d95a4f7de4f944c202cbe370d205f34 100644 (file)
 #else
 #include "player_interface.h"
 #include "player_device_walk.h"
+#include "player_device_skate.h"
 #include "player_model.h"
 
 /* temp */
 VG_STATIC player_interface localplayer;
 VG_STATIC struct player_device_walk localplayer_walk;
+VG_STATIC struct player_device_skate localplayer_skate;
 VG_STATIC struct player_avatar localplayer_avatar;
 VG_STATIC glmesh localplayer_meshes[3];
 
@@ -233,6 +235,7 @@ VG_STATIC void vg_load(void)
    player_use_avatar( &localplayer, &localplayer_avatar );
    player_use_mesh( &localplayer, &localplayer_meshes[0] );
    player_use_device( &localplayer, &player_device_walk, &localplayer_walk );
+   player_use_device( &localplayer, &player_device_skate, &localplayer_skate );
 
    /* --------------------- */
 
@@ -308,7 +311,7 @@ VG_STATIC void vg_update_post(void)
       }
 #endif
 
-      player_post_update( &localplayer );
+      player_post_update( &localplayer, &main_camera );
 
 #if 0
       menu_update();