Browse Source

Add quake post

master
Jens Pitkänen 7 months ago
parent
commit
dc0056c2e6
29 changed files with 7613 additions and 7 deletions
  1. +1
    -1
      Makefile
  2. +9
    -0
      NOTES.md
  3. +1
    -0
      public/2020/04/03/demo/dat.gui.css
  4. +13
    -0
      public/2020/04/03/demo/dat.gui.min.js
  5. +35
    -0
      public/2020/04/03/demo/demo.css
  6. +367
    -0
      public/2020/04/03/demo/demo.js
  7. +3187
    -0
      public/2020/04/03/demo/gltf-loader.js
  8. BIN
      public/2020/04/03/demo/scene.bin
  9. +2782
    -0
      public/2020/04/03/demo/scene.gltf
  10. BIN
      public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1cop1_2.png_baseColor.png
  11. BIN
      public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1cop1_7.png_baseColor.png
  12. BIN
      public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1cop3_4.png_baseColor.png
  13. BIN
      public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1lava1.png_baseColor.png
  14. BIN
      public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1light3_7.png_baseColor.png
  15. BIN
      public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1metal1_3.png_baseColor.png
  16. BIN
      public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1metal1_4.png_baseColor.png
  17. BIN
      public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1metal1_6.png_baseColor.png
  18. BIN
      public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1metal5_8.png_baseColor.png
  19. BIN
      public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1metal6_1.png_baseColor.png
  20. BIN
      public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1metal6_2.png_baseColor.png
  21. BIN
      public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1mmetal1_3.png_baseColor.png
  22. BIN
      public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1teleport.png_baseColor.png
  23. +1034
    -0
      public/2020/04/03/demo/three.min.js
  24. +8
    -0
      public/2020/04/03/fps-movement.csv
  25. +161
    -0
      public/2020/04/03/fps-movement.md
  26. BIN
      public/2020/04/03/sample.mp4
  27. BIN
      public/2020/04/03/sample.webm
  28. +14
    -5
      public/default.css
  29. +1
    -1
      templates/post.html

+ 1
- 1
Makefile View File

@@ -1,6 +1,6 @@
.POSIX:

POSTS = public/2019/10/27/hidpi.html public/2019/07/15/mastodown.html public/2019/07/06/make.html
POSTS = public/2020/04/03/fps-movement.html public/2019/10/27/hidpi.html public/2019/07/15/mastodown.html public/2019/07/06/make.html
POSTS_XML = $(POSTS:.html=.xml)
POSTS_PREVIEW = $(POSTS:.html=.preview.html)
RESOURCES = public/dark.css public/light.css public/feed.xml public/index.html


+ 9
- 0
NOTES.md View File

@@ -0,0 +1,9 @@
Here are notes that I will need later, when trying to figure out how
everything works:
- The templating system first pastes the post inside the template/*
file, then copies over fields from the post's csv.
- The csv's lines 1-6 are in use by the template/* files.
- The csv's line 7 is in use as a "root path" variable for posts that
need to refer to resources in their folders, while avoiding relative
links. Eg. `/2020/03/23/`, used in the context
`href="{{7}}sample.mp4"`.

+ 1
- 0
public/2020/04/03/demo/dat.gui.css
File diff suppressed because it is too large
View File


+ 13
- 0
public/2020/04/03/demo/dat.gui.min.js
File diff suppressed because it is too large
View File


+ 35
- 0
public/2020/04/03/demo/demo.css View File

@@ -0,0 +1,35 @@
canvas {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: -1;

/* Pixelated canvases according to: https://stackoverflow.com/questions/7615009/disable-interpolation-when-scaling-a-canvas */
image-rendering: optimizeSpeed; /* Older versions of FF */
image-rendering: -moz-crisp-edges; /* FF 6.0+ */
image-rendering: -webkit-optimize-contrast; /* Safari */
image-rendering: -o-crisp-edges; /* OS X & Windows Opera (12.02+) */
image-rendering: pixelated; /* Awesome future-browsers */
-ms-interpolation-mode: nearest-neighbor; /* IE */
}

.credits {
position: absolute;
left: 1em;
bottom: 1em;
}

.credits p, .credits a, .instructions p, .instructions a {
display: block;
margin: 0;
padding: 0;
line-height: 1.15;
}

.instructions {
position: absolute;
left: 1em;
top: 1em;
}

+ 367
- 0
public/2020/04/03/demo/demo.js View File

@@ -0,0 +1,367 @@
/* A first-person 3D environment demonstrating some aspects of Quake.
* Copyright (C) 2020 Jens Pitkanen
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

var scene, camera, renderer;
var level_object, raycaster;
var render_width, render_height;
var gui;
var dynamic_values = {
render_scale: 1.0,
camera_sensitivity: 3.0,
field_of_view: 75.0,
noclip: false,
gravity: 800.0,
movement_speed: 250.0,
jump_velocity: 250.0,
lean: { enable: true, lean_degrees: 2.0, lean_degs_per_sec: 200.0 },
accel: { enable: true, acceleration: 10.0, friction: 4.0, extra_friction_velocity_threshold: 100.0 },
bobbing: { enable: true, bob_freq: 0.6, bob_amplitude: 0.02, bob_base: 0.3 },
};

let player = {
position: new THREE.Vector3(200.0, -150.0, 70.0),
velocity: new THREE.Vector3(0.0, 0.0, 0.0),
camera_height: 50.0,
bob: 0.0,
radius: 10.0,
yaw: 0.0,
pitch: 0.0,
roll: 0.0,
grounded: false,
grounded_coyote_time: 0.0,
};

let controls = {
move_forward: { value: 0.0, target: 0.0 }, move_left: { value: 0.0, target: 0.0 },
move_backward: { value: 0.0, target: 0.0 }, move_right: { value: 0.0, target: 0.0 },
move_up: { value: 0.0, target: 0.0 }, move_down: { value: 0.0, target: 0.0 },
look_up: { value: 0.0, target: 0.0 }, look_left: { value: 0.0, target: 0.0 },
look_down: { value: 0.0, target: 0.0 }, look_right: { value: 0.0, target: 0.0 },
jump: false, debug_misc: false,
};

function update_input_state(delta_seconds) {
let lerped_controls = ["look_up", "look_left", "look_down", "look_right"];
for (let i = 0; i < lerped_controls.length; i++) {
let control = controls[lerped_controls[i]];
control.value = THREE.MathUtils.lerp(control.value, control.target, delta_seconds * 20);
}
let non_lerped_controls = ["move_forward", "move_left", "move_backward", "move_right", "move_up", "move_down"];
for (let i = 0; i < non_lerped_controls.length; i++) {
let control = controls[non_lerped_controls[i]];
control.value = control.target;
}
controls.jump = false;
controls.debug_misc = false;
}

var last_millis = 0;
function update(current_millis) {
let delta_seconds = (current_millis - last_millis) / 1000.0;
let total_seconds = current_millis / 1000.0;
last_millis = current_millis;
let render_scale = dynamic_values.render_scale;

let new_width = window.innerWidth * render_scale;
let new_height = window.innerHeight * render_scale;
if (render_width != new_width || render_height != new_height || camera.fov != dynamic_values.field_of_view) {
render_width = new_width;
render_height = new_height;
renderer.setSize(render_width, render_height, false);
camera.aspect = render_width / render_height;
camera.fov = dynamic_values.field_of_view;
camera.updateProjectionMatrix();
}

// Aim for the camera to be camera_height above the ground
let ground_direction = new THREE.Vector3(0, -player.camera_height, 0);
let ground = raycast_at(ground_direction, 0);
if (ground != undefined) {
let target = player.position.clone().add(new THREE.Vector3(0, player.camera_height - ground.distance, 0));
player.position.lerp(target, 20 * delta_seconds);
}
// Update groundedness
player.grounded = raycast_at(ground_direction, 1);
if (player.grounded) {
player.grounded_coyote_time = total_seconds;
}

// Calculate player movement vector, apply to velocity
let forward_input = controls.move_forward.value - controls.move_backward.value;
let right_input = controls.move_right.value - controls.move_left.value;
let up_input = dynamic_values.noclip ? controls.move_up.value - controls.move_down.value : 0;

let forward_move_vector = new THREE.Vector3(-Math.sin(player.yaw), 0, -Math.cos(player.yaw));
forward_move_vector.multiplyScalar(dynamic_values.movement_speed * forward_input);
let right_vector = new THREE.Vector3(Math.cos(player.yaw), 0, -Math.sin(player.yaw));
let right_move_vector = right_vector.clone();
right_move_vector.multiplyScalar(dynamic_values.movement_speed * right_input);
let up_move_vector = new THREE.Vector3(0, 1, 0);
up_move_vector.multiplyScalar(dynamic_values.movement_speed * up_input);
let movement_vector = new THREE.Vector3()
.add(forward_move_vector)
.add(right_move_vector)
.add(up_move_vector)
.normalize();

let previous_y_velocity = player.velocity.y;
if (dynamic_values.accel.enable && !dynamic_values.noclip) {
let speed = dynamic_values.movement_speed * movement_vector.length();
let max_accel = speed - player.velocity.dot(movement_vector);
if (max_accel > 0) {
let acceleration = Math.min(max_accel, dynamic_values.accel.acceleration * delta_seconds * speed);
player.velocity.add(movement_vector.multiplyScalar(acceleration));
}

if (player.grounded) {
let ground_velocity = player.velocity.clone();
ground_velocity.y = 0;
let ground_speed = ground_velocity.length();

// Apply friction
let control = Math.max(ground_speed, dynamic_values.accel.extra_friction_velocity_threshold);
let friction_factor = Math.max(0, (ground_speed - dynamic_values.accel.friction * delta_seconds * control) / ground_speed);
player.velocity.multiplyScalar(friction_factor);
}
} else if (!dynamic_values.accel.enable || dynamic_values.noclip) {
player.velocity = movement_vector.multiplyScalar(dynamic_values.movement_speed);
}

// In noclip, use input Y velocity, otherwise accumulate gravity
if (dynamic_values.noclip) {
player.velocity.y = movement_vector.y;
} else if (controls.jump && total_seconds - player.grounded_coyote_time < 0.1) {
player.velocity.y = dynamic_values.jump_velocity;
} else if (player.grounded) {
player.velocity.y = 0;
} else {
player.velocity.y = previous_y_velocity - dynamic_values.gravity * delta_seconds;
}

// For raycasting from the camera
function raycast_at(vector, threshold) {
if (dynamic_values.noclip) {
// No walls in noclip!
return undefined;
}

let camera_position = player.position.clone().add(new THREE.Vector3(0, player.camera_height, 0));
let direction = vector.clone().normalize();
raycaster.set(camera_position, direction);
raycaster.far = vector.length() + threshold;
return raycaster.intersectObject(level_object, true)[0];
}

// For stepping in a direction (but avoiding the camera going through walls with raycast_at)
function step_toward(velocity_step) {
if (velocity_step.length() > 0) {
let hit = raycast_at(velocity_step, 10);
if (hit != undefined) {
velocity_step.projectOnPlane(hit.face.normal);
}
player.position.add(velocity_step);
}
}

// Apply player movement (clamped based on a raycast)
let velocity_step = player.velocity.clone().multiplyScalar(delta_seconds);
step_toward(velocity_step.clone().projectOnVector(new THREE.Vector3(1, 0, 0)));
step_toward(velocity_step.clone().projectOnVector(new THREE.Vector3(0, 1, 0)));
step_toward(velocity_step.clone().projectOnVector(new THREE.Vector3(0, 0, 1)));

let yaw_velocity = (controls.look_left.value - controls.look_right.value) * delta_seconds;
let pitch_velocity = (controls.look_up.value - controls.look_down.value) * delta_seconds;
player.yaw += yaw_velocity * dynamic_values.camera_sensitivity;
player.pitch += pitch_velocity * dynamic_values.camera_sensitivity;
player.pitch = Math.min(Math.PI / 2, Math.max(-Math.PI / 2, player.pitch));

if (dynamic_values.lean.enable && !dynamic_values.noclip) {
let roll_sign = -player.velocity.dot(right_vector) / dynamic_values.movement_speed;
let rads = dynamic_values.lean.lean_degrees / 180 * Math.PI;
let speed = dynamic_values.lean.lean_degs_per_sec / 180 * Math.PI * delta_seconds;
let diff = roll_sign * rads - player.roll;
if (Math.abs(diff) > speed) {
diff = Math.sign(diff) * speed;
}
player.roll += diff;
}

if (dynamic_values.bobbing.enable && !dynamic_values.noclip) {
let xz_velocity = player.velocity.clone();
xz_velocity.y = 0;
let t = total_seconds * 2 * Math.PI / dynamic_values.bobbing.bob_freq;
player.bob = xz_velocity.length() * dynamic_values.bobbing.bob_amplitude;
player.bob = player.bob * dynamic_values.bobbing.bob_base + player.bob * (1 - dynamic_values.bobbing.bob_base) * Math.sin(t);
}

camera.position.x = player.position.x;
camera.position.y = player.position.y + player.camera_height + player.bob;
camera.position.z = player.position.z;

camera.setRotationFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
camera.rotateX(player.pitch);
if (dynamic_values.lean.enable) {
camera.rotateZ(player.roll);
}

renderer.render(scene, camera);
update_input_state(delta_seconds);
requestAnimationFrame(update);
}

function start() {
gui = new dat.GUI();
let gui_rendering = gui.addFolder("Rendering");
gui_rendering.add(dynamic_values, "render_scale", 0.25, 2.0);
gui_rendering.add(dynamic_values, "field_of_view", 50.0, 120.0);

let gui_camera = gui.addFolder("Camera");
gui_camera.add(dynamic_values, "camera_sensitivity");
gui_camera.add(dynamic_values, "noclip");

let gui_movement_basic = gui.addFolder("Basic Movement Vars");
gui_movement_basic.add(dynamic_values, "gravity");
gui_movement_basic.add(dynamic_values, "movement_speed");
gui_movement_basic.add(dynamic_values, "jump_velocity");

let gui_lean = gui.addFolder("Quake Observation: Leaning");
gui_lean.add(dynamic_values.lean, "enable");
gui_lean.add(dynamic_values.lean, "lean_degrees");
gui_lean.add(dynamic_values.lean, "lean_degs_per_sec");
let gui_accel = gui.addFolder("Quake Observation: Acceleration");
gui_accel.add(dynamic_values.accel, "enable");
gui_accel.add(dynamic_values.accel, "acceleration");
gui_accel.add(dynamic_values.accel, "friction");
gui_accel.add(dynamic_values.accel, "extra_friction_velocity_threshold");
let gui_bobbing = gui.addFolder("Quake Observation: Bobbing");
gui_bobbing.add(dynamic_values.bobbing, "enable");
gui_bobbing.add(dynamic_values.bobbing, "bob_freq");
gui_bobbing.add(dynamic_values.bobbing, "bob_amplitude");
gui_bobbing.add(dynamic_values.bobbing, "bob_base", 0, 1);

scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(dynamic_values.field_of_view, 1, 1, 2000);
renderer = new THREE.WebGLRenderer();
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.physicallyCorrectLights = true;
renderer.toneMapping = THREE.ReinhardToneMapping;
raycaster = new THREE.Raycaster();

document.body.appendChild(renderer.domElement);

let createElement = function(parent, name, text) {
let element = document.createElement(name);
element.textContent = text;
parent.appendChild(element);
return element;
};

let credits_root = createElement(document.body, "details", "");
credits_root.className = "credits";
let credits_header = createElement(credits_root, "summary", "Assets used:");
let level = createElement(credits_root, "a", "Quake level (recreation?) by vrchris, under the CC-BY-4.0 license");
level.href = "https://sketchfab.com/3d-models/quake-1-dm4-the-bad-place-deathmatch-b86569c202fa455b8d221e90c5588cc7";

let controls_root = createElement(document.body, "details", "");
controls_root.className = "instructions";
let controls_header = createElement(controls_root, "summary", "Controls:");
let controls_movement = createElement(controls_root, "p", "- Movement: WASD");
let controls_jump = createElement(controls_root, "p", "- Jump: Space");
let controls_look = createElement(controls_root, "p", "- Look: Arrow keys");
let controls_updown = createElement(controls_root, "p", "- Up / down (noclip): Space / left shift");

scene.add(new THREE.HemisphereLight(0xffffff, 0x0f0f0f, 10.0));

let gltf_loader = new THREE.GLTFLoader();
gltf_loader.load("scene.gltf", function(gltf) {
let lightMaterial = "DGamesQuakeid1pak0_filesmapse1m1metal5_8";
function recurse(mesh) {
// Process submesh
if (mesh.material !== undefined && mesh.material.name.startsWith(lightMaterial)) {
mesh.material.emissive = new THREE.Color(0xffffff);
mesh.material.emissiveMap = mesh.material.map;
mesh.material.emissiveIntensity = 1.5;
}

for (let i = 0; i < mesh.children.length; i++) {
recurse(mesh.children[i]);
}
};
recurse(gltf.scene);
level_object = gltf.scene;
scene.add(gltf.scene);

// Start the game:
update(0);
}, undefined, function(error) {
console.log(error);
});
}

function set_control_value(key_code, value) {
// WASD: 87, 65, 83, 68
// Arrow up/left/down/right: 38, 37, 40, 39
switch (key_code) {
case 32: controls.move_up.target = value; controls.jump = value > 0.5; return true;
case 16: controls.move_down.target = value; return true;
case 87: controls.move_forward.target = value; return true;
case 65: controls.move_left.target = value; return true;
case 83: controls.move_backward.target = value; return true;
case 68: controls.move_right.target = value; return true;
case 38: controls.look_up.target = value; return true;
case 37: controls.look_left.target = value; return true;
case 40: controls.look_down.target = value; return true;
case 39: controls.look_right.target = value; return true;
case 76: controls.debug_misc = value > 0.5; return true;
default:
return false;
}
}

document.body.onkeydown = function(event) {
if (set_control_value(event.keyCode, 1.0)) {
event.preventDefault();
}
}

document.body.onkeyup = function(event) {
if (set_control_value(event.keyCode, 0.0)) {
event.preventDefault();
}
}

// From: https://github.com/mrdoob/three.js/blob/master/examples/jsm/WebGL.js
function is_webgl_available() {
try {
var canvas = document.createElement("canvas");
return !! (window.WebGLRenderingContext && (canvas.getContext("webgl") || canvas.getContext("experimental-webgl")));
} catch (e) {
return false;
}
}

if (is_webgl_available()) {
start();
} else {
let warning_element = document.createElement("p");
warning_element.textContent = "Your browser seems to lack WebGL support. ";
let link_element = document.createElement("a");
link_element.textContent = "Get Firefox today!";
link_element.href = "https://www.mozilla.org/en-US/firefox/new/";
warning_element.appendChild(link_element);
document.body.appendChild(warning_element);
}

+ 3187
- 0
public/2020/04/03/demo/gltf-loader.js
File diff suppressed because it is too large
View File


BIN
public/2020/04/03/demo/scene.bin View File


+ 2782
- 0
public/2020/04/03/demo/scene.gltf
File diff suppressed because it is too large
View File


BIN
public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1cop1_2.png_baseColor.png View File

Before After
Width: 64  |  Height: 64  |  Size: 2.3 KiB

BIN
public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1cop1_7.png_baseColor.png View File

Before After
Width: 64  |  Height: 64  |  Size: 2.4 KiB

BIN
public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1cop3_4.png_baseColor.png View File

Before After
Width: 64  |  Height: 64  |  Size: 2.8 KiB

BIN
public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1lava1.png_baseColor.png View File

Before After
Width: 64  |  Height: 64  |  Size: 1.4 KiB

BIN
public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1light3_7.png_baseColor.png View File

Before After
Width: 32  |  Height: 32  |  Size: 646 B

BIN
public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1metal1_3.png_baseColor.png View File

Before After
Width: 64  |  Height: 64  |  Size: 2.3 KiB

BIN
public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1metal1_4.png_baseColor.png View File

Before After
Width: 64  |  Height: 64  |  Size: 2.1 KiB

BIN
public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1metal1_6.png_baseColor.png View File

Before After
Width: 64  |  Height: 64  |  Size: 1.6 KiB

BIN
public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1metal5_8.png_baseColor.png View File

Before After
Width: 32  |  Height: 64  |  Size: 2.1 KiB

BIN
public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1metal6_1.png_baseColor.png View File

Before After
Width: 32  |  Height: 64  |  Size: 1.0 KiB

BIN
public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1metal6_2.png_baseColor.png View File

Before After
Width: 32  |  Height: 64  |  Size: 1.1 KiB

BIN
public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1mmetal1_3.png_baseColor.png View File

Before After
Width: 64  |  Height: 64  |  Size: 1.3 KiB

BIN
public/2020/04/03/demo/textures/DGamesQuakeid1pak0_filesmapse1m1teleport.png_baseColor.png View File

Before After
Width: 64  |  Height: 64  |  Size: 1.2 KiB

+ 1034
- 0
public/2020/04/03/demo/three.min.js
File diff suppressed because it is too large
View File


+ 8
- 0
public/2020/04/03/fps-movement.csv View File

@@ -0,0 +1,8 @@
/2020/04/03/fps-movement
Quake studies
tag:blog.neon.moe,2020-04-03:/2020/04/03/fps-movement
2020-04-03
2020-04-03T07:29:00+03:00
2020-04-03T07:29:00+03:00
Or: I like Quake's movement, let's figure out how it works.
/2020/04/03/

+ 161
- 0
public/2020/04/03/fps-movement.md View File

@@ -0,0 +1,161 @@
## Quake studies

To start this off, I would like to make the disclaimer that I am not a
professional Quake player. In fact, I have only played Quake 1 and
Quake Live, and my combined playtime is in the magnitude of hours, not
tens or thousands.

That said, every now and then, I get the feeling to play Quake. One
should play the classics, right? The feeling never seems to last, but
I do always enjoy Quake's movement. Delightful to zoom around that
starting chamber. Since I am currently working on a first-person
puzzle game, I figure I should find out what makes Quake feel so good,
to replicate that feeling in my game.

### Observation: leaning

The most visually distinct "effect" in Quake is the way the camera
leans when strafing. This might also be happening when you move
forward and turn---which could imply that this lean is actually based
on the player's velocity on the right/left axis, and that turning has
a bit of inertia---but the effect is hard to detect by just
eyeballing. It should also be noted, that this lean does not apply to
the gun in the middle: it actually accentuates the lean by staying
upright.

<video width="400pt" controls loop preload="auto">
<source src="{{7}}sample.webm" type="video/webm">
<source src="{{7}}sample.mp4" type="video/mp4">
<p>
You can download the demonstration video here:
<a href="{{7}}sample.webm">WebM</a>,
<a href="{{7}}sample.mp4">MP4</a>.
</p>
</video>


### Observation: bobbing

The camera bobs slightly on the up/down axis, while the gun has a more
pronounced bob, down and towards the player. The gun doesn't seem to
bob in a sine wave either, like the camera: it seems to stay
relatively still, until it dips noticeably whenever the camera
dips. Modern gun bobs are more elaborate, but I do think there is a
certain charm to Quake's bob.

### Observation: acceleration

When releasing the movement keys, the player will slow to a stop over
a short period of time. I can not tell if there is acceleration when
you start moving, but if there is, it is very sharp. Still, moving
forward does feel very smooth, so maybe it does accelerate over a few
frames. This effect is even more pronounced in Quake Live: the
movement feels *really* smooth. I hope the source will enlighten me
further on this.

### Source

If you would like to check out the source for yourself, it is on [id's
GitHub][quake-src].

<div class="comment"> /* About the code: It is delightfully
readable. There is something about games made by big companies that
makes them seem intimidating to me, so it is always suprising to find
that they have been made by humans much like you and me. */ </div>

#### Leaning

Lucky me, the leaning code is the [first function of
view.c][view.c:81]. The lean is based on the player's velocity on the
right axis, as I suspected, and it seems linear. Simple to implement,
for a nice effect.

#### Bobbing

Head bobbing is implemented [in the very next function of
view.c][view.c:112]. The code is pretty much what you'd expect, with a
bobbing period of 0.6 seconds. The bobbing intensity is based on the
player's velocity on the XY plane. This is good for fading out the
effect, taking into account how quickly the player accelerates /
brakes.

Turns out, the gun does not actually bob up and down with varying
speed: it bobs in sync with the camera, *forwards and
backwards*. Because of how perspective projection works, the effect is
more pronounced as it gets closer to the camera, which led me to
believe the bob is not a sine wave, assuming the motion was
up/down. This is why it is great to have the source available!

<div class="comment"> /* Something I thought was interesting: there is
a commented bit of code that would make the gun sway horizontally as
well. */ </div>

#### Friction

Next up, from sv_user.c: [SV_UserFriction][sv_user.c:122].

The friction function is probably the spiciest code I have reviewed
for this post, it's easiest to demonstrate by just showing the code
(edited for readability):

```c
float speed = length(velocity.xz);

float control = speed < sv_stopspeed.value ? sv_stopspeed.value : speed;
float newspeed = (speed - host_frametime * control * friction) / speed;

if (newspeed < 0) newspeed = 0;

velocity *= newspeed;
```

Is that not wild? Maybe not. It is basically just like, friction, but
what gets me is the `control` variable. The amount of friction applied
goes down as your velocity decreases, until you pass the "stop speed"
threshold, after which the friction stays constant. This causes this
sort of braking effect in game, which I really enjoy.

#### Acceleration

The acceleration function is what you would expect, like the friction
was, but has no extra spices: [SV_Accelerate][sv_user.c:190]
calculates an acceleration value based on the target speed, then
applies that acceleration to the player's velocity. The acceleration
is *10 * frametime * wishspeed*, so *wishspeed* is reached in about a
tenth of a second, according to my napkin calculations. 100
milliseconds is many frames! I am not sure how I was fooled into
thinking the movement was so sharp I could not tell if there was
acceleration, but testing it out now, the acceleration is quite
clear. Never trust eyeballed observations!

### Demo

I originally intended on putting the demo right here, but I thought
it'd be better to warn you before loading up a game in your
browser. So be warned, the demo link will open a web game written in
JavaScript, with [three.js][three.js]. Controls: WASD to move, arrow
keys to look around, space to jump. Mind the non-axis-aligned walls,
they are not quite solid. [Demo][demo].

### Final thoughts

What did I learn? That I should never trust my own judgement on
movement. Also, that a good acceleration function combined with a good
friction function make for some good movement! With a little bobbing
and leaning sprinkled on top, you get some *excellent* movement out of
a few lines of code.

I hope you learned something, or were entertained. Nevertheless, if
you have comments, feel free to aim them at my [Fediverse inbox][fedi]
or [my email][email].

[quake-src]: https://github.com/id-software/quake "Quake on GitHub"
[view.c]: https://github.com/id-software/quake/tree/master/WinQuake/view.c "WinQuake/view.c on GitHub"
[view.c:81]: https://github.com/id-software/quake/tree/master/WinQuake/view.c#L81 "WinQuake/view.c line 81 (V_CalcRoll) on GitHub"
[view.c:112]: https://github.com/id-software/quake/tree/master/WinQuake/view.c#L112 "WinQuake/view.c line 112 (V_CalcBob) on GitHub"
[sv_user.c:122]: https://github.com/id-software/quake/tree/master/WinQuake/sv_user.c#L122 "WinQuake/sv_user.c line 122 (SV_UserFriction)on GitHub"
[sv_user.c:190]: https://github.com/id-software/quake/tree/master/WinQuake/sv_user.c#L190 "WinQuake/sv_user.c line 190 (SV_Accelerate) on GitHub"
[three.js]: https://threejs.org "The homepage of the Three.js 3D rendering library"
[demo]: demo/index.html "An FPS demo, showing off ideas written about in this blog"
[fedi]: https://fedi.neon.moe/neon "My fediverse account"
[email]: mailto:jens@neon.moe "My public email"

BIN
public/2020/04/03/sample.mp4 View File


BIN
public/2020/04/03/sample.webm View File


+ 14
- 5
public/default.css View File

@@ -69,9 +69,10 @@ a:visited { color: #7600D6; }
a:hover { color: #4C00EB; }
a:active { color: #EB005E; }
.comment { color: #666; }
code {
code, video {
background-color: #EEE;
border-color: #AAA;
color: #444;
}

@media (prefers-color-scheme: dark) {
@@ -82,7 +83,7 @@ code {
a:hover { color: #A4EB00; }
a:active { color: #00EB8C; }
.comment { color: #BBB; }
code {
code, video {
background-color: #444;
border-color: #777;
color: #DDD;
@@ -168,7 +169,15 @@ pre code {
@media (max-width: 30rem) {
/* But when you're on mobile or otherwise have a narrow client, it's
better to just have the code block be as wide as it can. */
pre code {
margin-right: 0;
}
pre code { margin-right: 0; }
}

/* Video container styling */
video {
margin: 0;
padding: 0;
border-width: 2px;
border-style: solid;
width: 97%;
width: calc(100% - 1em);
}

+ 1
- 1
templates/post.html View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>~neon/thoughts{{0}}</title>
<title>{{1}} | ~neon/thoughts</title>
<link href="/feed.xml" rel="alternate" type="application/atom+xml" title="This blog's Atom feed" />
<link href="/default.css" rel="stylesheet" type="text/css" title="Default" />
<link href="/dark.css" rel="alternate stylesheet" type="text/css" title="Dark" />


Loading…
Cancel
Save