Skip to content

Commit afbe80d

Browse files
committed
Fix description of impulses, add info about palettes and conversions
1 parent 3b4dc5d commit afbe80d

File tree

3 files changed

+41
-15
lines changed

3 files changed

+41
-15
lines changed

docs/_posts/2019-12-12-data-formats.md

+20-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ title: Data Formats
55

66
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.
77

8+
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.
9+
810
## Level
911

1012
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:
@@ -63,4 +65,21 @@ NameID RiverBier
6365
Finally, this is a line from `wrlds.dat`:
6466
```
6567
Necross thechain/necross/world.ini
66-
```
68+
```
69+
70+
## Palette
71+
72+
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:
73+
74+
![level layers]({{site.baseurl}}/assets/palette.png)
75+
76+
You can see 8 different sections here corresponding to the different types of terrain.
77+
78+
## Modding
79+
80+
[Vange-rs](https://github.com/kvark/vange-rs) project has a `convert` binary that can be used for conversions into and from popular formats:
81+
- Model to/from Wavefront OBJ with RON metadata
82+
- Level to/from multiple PNGs with RON metadata
83+
- Palette to/from PNG
84+
85+
See the [Readme section](https://github.com/kvark/vange-rs#converter) for the exact commands.

docs/_posts/2019-12-17-collision-model.md

+21-14
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ These parameters are updated regularly with regards to the forces, impulses, tim
1919

2020
### Impulses
2121

22-
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:
22+
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:
23+
{: #impulse-equation }
2324
```rust
24-
let collision_vector = linear_velocity + cross(angular_velocity, collision_point);
25+
let point_velocity = linear_velocity + cross(angular_velocity, collision_point);
26+
let collision_vector = SOME_CONSTANTS * collision_direction * dot(point_velocity, collision_direction);
2527
```
2628

27-
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:
29+
In order to evaluate the impulse, we then compute the local collision matrix:
2830

2931
```cpp
3032
mat3 calc_collision_matrix_inv(vec3 r, mat3 ji) {
@@ -40,7 +42,7 @@ mat3 calc_collision_matrix_inv(vec3 r, mat3 ji) {
4042
}
4143
```
4244
43-
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.
45+
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.
4446
4547
Once the local collision matrix is computed, the raw impulse can be derived as:
4648
```rust
@@ -55,28 +57,27 @@ angular_velocity += jacobian_inv * cross(collision_point, impulse);
5557
### Forces
5658

5759
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:
60+
{: #force-equation }
5861
```rust
59-
fn apply_force(vector, point) {
62+
fn apply_force(point, vector) {
6063
linear_force += vector;
6164
angular_force += cross(point, vector);
6265
}
6366
```
6467

6568
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.
6669

67-
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`.
70+
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`.
6871

69-
Note: this part that may need to take the time delta into account in order to have smooth physics simulation with variable frame rate.
70-
71-
Finally, local effects like vortexes may also contribute to the forces.
72+
Finally, local effects like vortexes or artifact abilities may also contribute to the forces.
7273

7374
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:
7475
```rust
7576
linear_velocity += time_delta * linear_forces;
7677
angular_velocity += time_delta * jacobian_inv * angular_forces;
7778
```
7879

79-
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:
80+
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:
8081
- for forces, they are pre-multiplied by `time_delta`
8182
- for impulses, the angular component comes from the `cross(point, vector)`
8283
- multiplication by `jacobian_inv` is done only once at the end of the simulation step
@@ -120,18 +121,24 @@ At the end of the step (after the position and orientation are updated), the vel
120121

121122
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.
122123

123-
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:
124+
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:
124125

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

127128
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.
128129

129130
### Controls
130131

131-
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):
132+
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):
132133
```rust
133-
let rudder_vec = vec3(cos(car_rudder), -sin(car_rudder), 0.0);
134-
let projected_collision_vector = rudder_vec * dot(collision_vector, rudder_vec);
134+
let wheel_collision_direction = vec3(cos(car_rudder), -sin(car_rudder), 0.0);
135+
```
136+
137+
Jumps are implemented as modification of the linear velocity based on the mass:
138+
```rust
139+
fn jump(power) {
140+
linear_velocity += power * LOCAL_JUMP_DIRECTION / pow(car_mass, 0.3);
141+
}
135142
```
136143

137144
### Simulation Loop

docs/assets/palette.png

1.1 KB
Loading

0 commit comments

Comments
 (0)