Skip to content

Commit

Permalink
Fix description of impulses, add info about palettes and conversions
Browse files Browse the repository at this point in the history
  • Loading branch information
kvark committed Dec 18, 2019
1 parent 3b4dc5d commit afbe80d
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 15 deletions.
21 changes: 20 additions & 1 deletion docs/_posts/2019-12-12-data-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ title: Data Formats

The original game features vast levels that were destructible and "live": there was often something moving underground, gates opening and closing, seasons changing, making the world feel very natural. But how could all of this be even stored in memory of a machine from the last century? 128Mb of RAM was supposed to be enough for the whole system running the game.

For the technical breakdown of the formats please refer to the [wiki page](https://github.com/kvark/vange-rs/wiki). This blog post focuses on intuitive description that allows reasoning about the data.

## Level

Level data is stored and maintained on a per-texel as a multi-layer height map with metadata. Every point of potentially 4096x32768 resolution of a level has its own 1-byte height and 1-byte metadata. For areas that are double-layered, two horizontally adjacent cells are merged to represent the 2x1 segment of the map. They encode the following values:
Expand Down Expand Up @@ -63,4 +65,21 @@ NameID RiverBier
Finally, this is a line from `wrlds.dat`:
```
Necross thechain/necross/world.ini
```
```

## Palette

The game has various color tables for objects and levels, depending on the current season (yes, the game had dynamic season change in 1998). Palettes are stored as 256 triples of bytes, each representing a color component that for some reason is divided by 4:

![level layers]({{site.baseurl}}/assets/palette.png)

You can see 8 different sections here corresponding to the different types of terrain.

## Modding

[Vange-rs](https://github.com/kvark/vange-rs) project has a `convert` binary that can be used for conversions into and from popular formats:
- Model to/from Wavefront OBJ with RON metadata
- Level to/from multiple PNGs with RON metadata
- Palette to/from PNG

See the [Readme section](https://github.com/kvark/vange-rs#converter) for the exact commands.
35 changes: 21 additions & 14 deletions docs/_posts/2019-12-17-collision-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ These parameters are updated regularly with regards to the forces, impulses, tim

### Impulses

First, we consider all collisions and pushes from another objects as well as the terrain, in order to have a list of impulses that affect the body. Collision vector is computed from the current velocities and the point of contact:
First, we consider all collisions and pushes from another objects as well as the terrain, in order to have a list of impulses that affect the body. The collision vector is computed as a projection of the current velocity at the collision point, projected onto the direction of the collision:
{: #impulse-equation }
```rust
let collision_vector = linear_velocity + cross(angular_velocity, collision_point);
let point_velocity = linear_velocity + cross(angular_velocity, collision_point);
let collision_vector = SOME_CONSTANTS * collision_direction * dot(point_velocity, collision_direction);
```

The collision vector is scaled based on some constants that determine the collision power. In order to evaluate the impulse, we then compute the local collision matrix:
In order to evaluate the impulse, we then compute the local collision matrix:

```cpp
mat3 calc_collision_matrix_inv(vec3 r, mat3 ji) {
Expand All @@ -40,7 +42,7 @@ mat3 calc_collision_matrix_inv(vec3 r, mat3 ji) {
}
```
Here, `r` is the point of contact, and `ji` is an inverted Jacobian matrix that is adjusted for volume and scale. These come from the physics part of the model data as described in the [Data Formats]({{site.baseurl}}/{% post_url 2019-12-12-data-formats %}). My understanding is that the matrix represents an approximation of the shape of an object, and the fact that it may respond differently to collisions coming from different directions.
Here, `r` is the point of contact, and `ji` is an inverted Jacobian matrix that is adjusted for volume and scale. These come from the physics part of the model data as described in the [Data Formats]({{site.baseurl}}/{% post_url 2019-12-12-data-formats %}). My understanding is that the matrix represents an approximation of the distribution of mass in an object, which makes it react respond differently to collisions coming from different directions.
Once the local collision matrix is computed, the raw impulse can be derived as:
```rust
Expand All @@ -55,28 +57,27 @@ angular_velocity += jacobian_inv * cross(collision_point, impulse);
### Forces

Forces are also tracked separately for translation and rotation. Whenever a force vector affects the body at a particular point, we compute the linear and angular components as follows:
{: #force-equation }
```rust
fn apply_force(vector, point) {
fn apply_force(point, vector) {
linear_force += vector;
angular_force += cross(point, vector);
}
```

First force that is always present is gravity. It's applied at point `(0, 0, z_offset_of_mass_center)`, which comes from the model parameters.

Interestingly, collisions also affect forces, or more specifically - the spring force. It corresponds to some in-game machinery of a car that puts pressure in all directions and can be activated for a jump. Spring force is applied at `collision_point` with `collision_vector`.
Interestingly, collisions also affect forces, or more specifically - the spring force. It corresponds to some in-game machinery of a car that puts pressure in all directions and can be activated for a jump. Spring force is applied [as the force equation](#force-equation) at `collision_point` with `collision_vector`.

Note: this part that may need to take the time delta into account in order to have smooth physics simulation with variable frame rate.

Finally, local effects like vortexes may also contribute to the forces.
Finally, local effects like vortexes or artifact abilities may also contribute to the forces.

Before the forces can translate into the velocity change, we need to make sure they are converted into the local space. The application is done as follows:
```rust
linear_velocity += time_delta * linear_forces;
angular_velocity += time_delta * jacobian_inv * angular_forces;
```

So technically a force works the same way as an impulse integrated over time, as if the `collision_vector` is given (as opposed to being computed based on the velocities), which makes the whole model rather elegant in my eyes. In `vange-rs`, both paths go through a "raw impulse" representation that is common between forces and impulses:
So technically a force works the same way as an impulse integrated over time, as if the `collision_vector` is given (as opposed to being computed), which makes the whole model rather elegant in my eyes. In `vange-rs`, both paths go through a "raw impulse" representation that is common between forces and impulses:
- for forces, they are pre-multiplied by `time_delta`
- for impulses, the angular component comes from the `cross(point, vector)`
- multiplication by `jacobian_inv` is done only once at the end of the simulation step
Expand Down Expand Up @@ -120,18 +121,24 @@ At the end of the step (after the position and orientation are updated), the vel

Remember the collision shapes we described in the [Data Formats]({{site.baseurl}}/{% post_url 2019-12-12-data-formats %})? These simplified quad-based mesh approximations get intersected with the terrain at each step. How? By just rasterizing them on the terrain and sampling the heights (and metadata) at each intersection point.

For each collision shape quad, we find the average in the penetration depth (along the Z axis) as well as the point of contact. Then we simply generate an impulse at that point with the vector pointing downwards, following the regular impulse equations. This picture shows the averaged contact points and vectors:
For each collision shape quad, we find the average in the penetration depth (along the Z axis) as well as the point of contact. Then we simply generate an impulse at that point with the vector pointing downwards, following the regular [impulse equations](#impulse-equation). This picture shows the averaged contact points and vectors:

![terrain collision vectors]({{site.baseurl}}/assets/terrain-collision-vectors.jpg)

Note: more precisely, the pixels are split into groups for "soft" contacts and "hard" constants, and these averaged contact points are used differently for some logic, like the horizontal wall collisions.

### Controls

User-controlled car is also affected by the traction, which is computed separately for each wheel. The basic logic is similar to a regular impulse computed at the wheel position (based on the current velocities), but with the collision vector projected onto the rudder vector (steering direction):
User-controlled car is also affected by the traction, which is computed separately for each wheel. Each wheel generates an impulse at its position along the rudder vector, following the same [impulse equation](#impulse-equation):
```rust
let rudder_vec = vec3(cos(car_rudder), -sin(car_rudder), 0.0);
let projected_collision_vector = rudder_vec * dot(collision_vector, rudder_vec);
let wheel_collision_direction = vec3(cos(car_rudder), -sin(car_rudder), 0.0);
```

Jumps are implemented as modification of the linear velocity based on the mass:
```rust
fn jump(power) {
linear_velocity += power * LOCAL_JUMP_DIRECTION / pow(car_mass, 0.3);
}
```

### Simulation Loop
Expand Down
Binary file added docs/assets/palette.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit afbe80d

Please sign in to comment.