SpaceX Crew Dragon paper model

Sometime ago I discovered the AXM Paper Space Scale Models website. On the website Alfonso X Moreno published various paper scale models of the Space Shuttle, the ISS, the Tiangong, SpaceX Starship, and others.

I downloaded the SpaceX Crew Dragon paper model and I donated a bit of money for it. I used pdftk to split the PDF document into single- and double-sided jobs. Then I used to get it printed, because they offer printing on 160gsm paper (normal office paper is only 80gsm and is not strong enough).

I couldn't find instructions on the website on how to build the docking version of the paper model. Finally I found an instructional video on Youtube:

Here is the finished Crew Dragon capsule (I will do the service module later).

I can really recommend to check out AXM's website if you are into paper models.

Enjoy and Merry Christmas!

Creating harmonica tabs with Lilypond

I play a bit of music using a Hohner Special 20 bluesharp. However I am not quick enough at reading sheet music, so I need to add tabs. Tabs basically tell you the number of the hole to play and whether to blow or draw (-). Lilypond is a concise programming language to generate sheet music. It turns out, that it is very easy to add lyrics using Lilypond. By putting the tabs in double quotes it is possible to add the tabs to the music. I found the tune Hard Times on the Internet and I used Lilypond to create a version with tabs. Here is the Lilypond code:

\version "2.22.0"
\header {
  title = "Hard Times"
  composer = "Stephen Foster"
  instrument = "Bluesharp"
\relative {
  r2 c'4 d
  e2 e4 d
  e8 g4.~ g4 e
  d4 c c d8 c
  e2 c'4 a
  g2 e8 c4.
  d4. c8 e4 d
  c2 c4 d
  e2 e4 d
  e8 g4.~ g4 e
  d4 c c d8 c
  e2 c'4 a
  g2 e8 c4.
  d4. c8 e4 d
  c2 e4 f4
  g2. g4
  g2 f4 g4
  g2 r2
  c2 a8 g4.
  e2 d8 c4.
  d4. c8 d4 e
  d2 c4 d
  e2 e4 d
  e8 g4.~ g4 e
  d4 c c d8 c
  e2 c'4 a
  g2 e8 c4.
  d4. c8 e4 d
  c4 r4 r2

\addlyrics {
  "4" "-4" "5" "5" "-4" "5" "6" "5" "-4" "4" "4" "-4" "4" "5" "7" "-6" "6" "5" "4" "-4" "4" "5" "-4" "4"
  "4" "-4" "5" "5" "-4" "5" "6" "5" "-4" "4" "4" "-4" "4" "5" "7" "-6" "6" "5" "4" "-4" "4" "5" "-4" "4"
  "5" "-5" "6" "6" "6" "-5" "6" "-6" "6" "7" "-6" "6" "5" "-4" "4" "-4" "4" "-4" "5" "-4"
  "4" "-4" "5" "5" "-4" "5" "6" "5" "-4" "4" "4" "-4" "4" "5" "7" "-6" "6" "5" "4" "-4" "4" "5" "-4" "4"

Here is the output generated by Lilypond. Notice how the tabs and the notes are aligned to each other. You can click on the image to open the PDF file.

Hard Times music sheet

By the way, I can really recommend to sign up for David Barrett's harmonica videos to learn tongue blocking which is essential to play single notes as in this example.



I added harmonica tabs for Silent Night:

Silent Night

Reversed-Z Rendering in OpenGL

By default the OpenGL 3D normalized device coordinates (NDC) are between -1 and +1. x is from left to right and y is from bottom to top of the screen while the z-axis is pointing into the screen.

normalized device coordinates

Usually the 3D camera coordinates of the object are mapped such that near coordinates are mapped to -1 and far values are mapped to +1. The near and far plane define the nearest and farthest point visualized by the rendering pipeline.

The absolute accuracy of floating point values is highest when the values are near zero. This leads to the depth accuracy to be highest somewhere between the near and far plane which is suboptimal.

A better solution is to map near coordinates to 1 and far coordinates to 0. In this case the higher precision near zero is used to compensate for the large range of distances covered towards the far plane.

zero to one mapping

Fortunately OpenGL 4.5 provides a way to perform reversed-z rendering:

using 0 to 1 z values

Let x, y, z, 1 be the homogeneous coordinate of the 3D point and x', y', z', w' the homogeneous NDC. Normally the 3D point uses a right-handed coordinate system where the z-axis points out of the screen.


This means that x and y are projected using negative z, i.e. w' = -z.

The camera projection equations are x'/w' = fw x/-z and y'/w' = fh y/-z.

Substituting w' = -z we get x' = fw x and y' = fh y.

Setting z' = a + b z we get the following matrix equation:

When z = -near, we want z'/w' = 1.

When z = -far, we want z'/w' = 0.

For z = -near we get: z' = w' => a + b z = -z => a - b near = near.

For z = -far we get: z' = 0 => a + b z = 0 => a - b far = 0 <=> b = a / far.

Substituting b we get: a - a near / far = near <=> a = near far / (far - near)

This means b = a / far = near / (far - near).

I.e. we get:


Finally we set fw = 1 / tan(fov / 2) where fov is the horizontal field of view angle (e.g. 60 degrees in radians).

fh then needs to be chosen such that fh / fw = w / h, where w and h are the width and height of the screen.

Installing Debian 11 (bullseye) on a physical machine

This is a summary on how to install Debian on a physical machine (as opposed to a VM).

I installed Debian 11 with KDE desktop on my HP 255 G3 notebook.

Because my computer has an AMD GPU it is recommended to download the unofficial Debian image with firmware. The unofficial image also helps with Wireless adapters which frequently require proprietary firmware. I downloaded the image which is 4 GByte in size and wrote it to a USB stick:

sudo dd if=firmware-11.0.0-amd64-DVD-1.iso of=/dev/sdb

Replace /dev/sdb with your correct USB location! You can see the location in the log (run sudo dmesg) after plugging it in. Under Windows you can use a tool such as Rufus to create a bootable USB stick.

I usually create a list of packages of my current system so that I can later install the same packages on the new system.

aptitude search '!~M ~i' -F '%p' > install.txt

I rebooted the computer. My laptop uses UEFI secure boot so you need to press Escape and then F9 to get a list of bootable devices. The Debian installer image on the USB stick should show up in the list. Select it and press return. I proceeded to use the graphical installer.

The Debian installer can shrink Windows partitions if you want to set up dual boot. Note that you need to disable Windows fast startup, otherwise your Windows file system will be in a hibernated state.

On my machine I created a swap partition about the same size as the computer memory. I already have a 16 GByte root (/) ext4 partition (should be a bit bigger ideally) and a large home (/home) ext4 partition. I just format the root partition and simply mount the home partition without formatting. After setting up the users I can already continue using the system.

I noticed that even the unofficial image is missing some firmware. So I cloned the Linux firmware repository and copied the firmware files:

git clone --depth 1
cd linux-firmware
sudo make install

I then proceeded with the instructions on how to set up Plymouth boot splash.

Furthermore I added to the package sources and updated the multimedia software.

Finally a few more tweaks:

  • I installed plasma-browser-integration and the corresponding browser addon (available for Firefox and Chrome/Chromium)
  • I installed ffmpegthumbs and kdegraphics-thumbnailers and then enabled the thumbnail generators in the Dolphin settings
  • I installed bleachbit and run it to clean up unused disk space
  • You can reduce the system swappiness to 10
  • In the BIOS I was able to change the order of the UEFI boot files so that Debian/Grub is started by default

I hope this installation summary is useful to you! Enjoy!

Tessellation OpenGL example using Clojure and LWJGL

Last week I published a minimal OpenGL example in Clojure. The example was implemented using LWJGL version 2 (see LWJGL Wiki for documentation). My motivation is to implement a space flight game, which requires rendering of planetary bodies. I found an impressive free software called the Oreon Engine which is implemented in Java. Watching the videos I realised that I should investigate tessellation shaders. I didn't find any small single-page code examples for tessellation shaders, so I decided to publish one here. The example is implemented in Clojure but could be easily ported to C if you prefer.

The example uses several shaders required when doing tessellation using OpenGL:

  • a vertex shader
  • a tessellation control shader
  • a tessellation evaluation shader
  • a geometry shader
  • a fragment shader

The example not only shows how to set the tessellation level but it also shows how one can pass through texture coordinates. The polygon mode was switched to display lines only so that one can observe how the triangle is split up.

See code below:

(ns tessellation-opengl
  (:import [org.lwjgl BufferUtils]
           [org.lwjgl.opengl Display DisplayMode GL11 GL12 GL13 GL15 GL20 GL30 GL32 GL40]))

(def vertex-source "#version 410 core
in mediump vec3 point;
in mediump vec2 texcoord;
out mediump vec2 texcoord_tcs;
void main()
  gl_Position = vec4(point, 1);
  texcoord_tcs = texcoord;

(def tcs-source "#version 410 core
layout(vertices = 4) out;
in mediump vec2 texcoord_tcs[];
out mediump vec2 texcoord_tes[];
void main(void)
  if (gl_InvocationID == 0) {
    gl_TessLevelOuter[0] = 2.0;
    gl_TessLevelOuter[1] = 3.0;
    gl_TessLevelOuter[2] = 4.0;
    gl_TessLevelOuter[3] = 5.0;
    gl_TessLevelInner[0] = 6.0;
    gl_TessLevelInner[1] = 7.0;
  gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
  texcoord_tes[gl_InvocationID] = texcoord_tcs[gl_InvocationID];

(def tes-source "#version 410 core
layout(quads, equal_spacing, ccw) in;
in mediump vec2 texcoord_tes[];
out mediump vec2 texcoord_geo;
void main()
  vec4 a = mix(gl_in[0].gl_Position, gl_in[1].gl_Position, gl_TessCoord.x);
  vec4 b = mix(gl_in[3].gl_Position, gl_in[2].gl_Position, gl_TessCoord.x);
  gl_Position = mix(a, b, gl_TessCoord.y);
  vec2 c = mix(texcoord_tes[0], texcoord_tes[1], gl_TessCoord.x);
  vec2 d = mix(texcoord_tes[3], texcoord_tes[2], gl_TessCoord.x);
  texcoord_geo = mix(c, d, gl_TessCoord.y);

(def geo-source "#version 410 core
layout(triangles) in;
in mediump vec2 texcoord_geo[3];
layout(triangle_strip, max_vertices = 3) out;
out mediump vec2 UV;
void main(void)
  gl_Position = gl_in[0].gl_Position;
  UV = texcoord_geo[0];
  gl_Position = gl_in[1].gl_Position;
  UV = texcoord_geo[1];
  gl_Position = gl_in[2].gl_Position;
  UV = texcoord_geo[2];

(def fragment-source "#version 410 core
in mediump vec2 UV;
out mediump vec3 fragColor;
uniform sampler2D tex;
void main()
  fragColor = texture(tex, UV).rgb;

(def vertices
  (float-array [ 0.5  0.5 0.0 1.0 1.0
                -0.5  0.5 0.0 0.0 1.0
                -0.5 -0.5 0.0 0.0 0.0
                 0.5 -0.5 0.0 1.0 0.0]))

(def indices
  (int-array [0 1 2 3]))

(def pixels
  (float-array [0.0 0.0 1.0
                0.0 1.0 0.0
                1.0 0.0 0.0
                1.0 1.0 1.0]))

(defn make-shader [source shader-type]
  (let [shader (GL20/glCreateShader shader-type)]
    (GL20/glShaderSource shader source)
    (GL20/glCompileShader shader)
    (if (zero? (GL20/glGetShaderi shader GL20/GL_COMPILE_STATUS))
      (throw (Exception. (GL20/glGetShaderInfoLog shader 1024))))

(defn make-program [& shaders]
  (let [program (GL20/glCreateProgram)]
    (doseq [shader shaders] (GL20/glAttachShader program shader))
    (GL20/glLinkProgram program)
    (if (zero? (GL20/glGetProgrami program GL20/GL_LINK_STATUS))
      (throw (Exception. (GL20/glGetProgramInfoLog program 1024))))

(defmacro def-make-buffer [method create-buffer]
  `(defn ~method [data#]
     (let [buffer# (~create-buffer (count data#))]
       (.put buffer# data#)
       (.flip buffer#)

(def-make-buffer make-float-buffer BufferUtils/createFloatBuffer)
(def-make-buffer make-int-buffer BufferUtils/createIntBuffer)

(Display/setTitle "mini")
(Display/setDisplayMode (DisplayMode. 320 240))

(def vertex-shader (make-shader vertex-source GL20/GL_VERTEX_SHADER))
(def fragment-shader (make-shader fragment-source GL20/GL_FRAGMENT_SHADER))
(def tcs-shader (make-shader tcs-source GL40/GL_TESS_CONTROL_SHADER))
(def tes-shader (make-shader tes-source GL40/GL_TESS_EVALUATION_SHADER))
(def geo-shader (make-shader geo-source GL32/GL_GEOMETRY_SHADER))
(def program (make-program vertex-shader fragment-shader geo-shader tcs-shader tes-shader))

(def vao (GL30/glGenVertexArrays))
(GL30/glBindVertexArray vao)

(def vbo (GL15/glGenBuffers))
(GL15/glBindBuffer GL15/GL_ARRAY_BUFFER vbo)
(def vertices-buffer (make-float-buffer vertices))
(GL15/glBufferData GL15/GL_ARRAY_BUFFER vertices-buffer GL15/GL_STATIC_DRAW)

(def idx (GL15/glGenBuffers))
(GL15/glBindBuffer GL15/GL_ELEMENT_ARRAY_BUFFER idx)
(def indices-buffer (make-int-buffer indices))
(GL15/glBufferData GL15/GL_ELEMENT_ARRAY_BUFFER indices-buffer GL15/GL_STATIC_DRAW)

(GL20/glVertexAttribPointer (GL20/glGetAttribLocation program "point"   ) 3 GL11/GL_FLOAT false (* 5 Float/BYTES) (* 0 Float/BYTES))
(GL20/glVertexAttribPointer (GL20/glGetAttribLocation program "texcoord") 2 GL11/GL_FLOAT false (* 5 Float/BYTES) (* 3 Float/BYTES))
(GL20/glEnableVertexAttribArray 0)
(GL20/glEnableVertexAttribArray 1)

(GL20/glUseProgram program)

(def tex (GL11/glGenTextures))
(GL13/glActiveTexture GL13/GL_TEXTURE0)
(GL11/glBindTexture GL11/GL_TEXTURE_2D tex)
(GL20/glUniform1i (GL20/glGetUniformLocation program "tex") 0)
(def pixel-buffer (make-float-buffer pixels))
(GL11/glTexImage2D GL11/GL_TEXTURE_2D 0 GL11/GL_RGB 2 2 0 GL12/GL_BGR GL11/GL_FLOAT pixel-buffer)
(GL30/glGenerateMipmap GL11/GL_TEXTURE_2D)

(GL11/glPolygonMode GL11/GL_FRONT_AND_BACK GL11/GL_LINE)

(while (not (Display/isCloseRequested))
  (GL11/glClearColor 0.0 0.0 0.0 0.0)
  (GL40/glPatchParameteri GL40/GL_PATCH_VERTICES 4)
  (GL11/glDrawElements GL40/GL_PATCHES 4 GL11/GL_UNSIGNED_INT 0)
  (Thread/sleep 40))

(GL20/glDisableVertexAttribArray 1)
(GL20/glDisableVertexAttribArray 0)

(GL11/glBindTexture GL11/GL_TEXTURE_2D 0)
(GL11/glDeleteTextures tex)

(GL15/glDeleteBuffers idx)

(GL15/glBindBuffer GL15/GL_ARRAY_BUFFER 0)
(GL15/glDeleteBuffers vbo)

(GL30/glBindVertexArray 0)
(GL30/glDeleteVertexArrays vao)

(GL20/glDetachShader program vertex-shader)
(GL20/glDetachShader program fragment-shader)
(GL20/glDeleteProgram program)
(GL20/glDeleteShader vertex-shader)
(GL20/glDeleteShader fragment-shader)



Any feedback, comments, and suggestions are welcome.