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 quad 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];
    EmitVertex();   
    gl_Position = gl_in[1].gl_Position;
  UV = texcoord_geo[1];
    EmitVertex();
    gl_Position = gl_in[2].gl_Position;
  UV = texcoord_geo[2];
    EmitVertex();
    EndPrimitive();
}")

(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))))
    shader))

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

(defmacro def-make-buffer [method create-buffer]
  `(defn ~method [data#]
     (let [buffer# (~create-buffer (count data#))]
       (.put buffer# data#)
       (.flip buffer#)
       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))
(Display/create)

(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)
(GL11/glTexParameteri GL11/GL_TEXTURE_2D GL11/GL_TEXTURE_WRAP_S GL12/GL_CLAMP_TO_EDGE)
(GL11/glTexParameteri GL11/GL_TEXTURE_2D GL11/GL_TEXTURE_WRAP_T GL12/GL_CLAMP_TO_EDGE)
(GL11/glTexParameteri GL11/GL_TEXTURE_2D GL11/GL_TEXTURE_MIN_FILTER GL11/GL_NEAREST)
(GL11/glTexParameteri GL11/GL_TEXTURE_2D GL11/GL_TEXTURE_MAG_FILTER GL11/GL_NEAREST)
(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)
  (GL11/glClear (bit-or GL11/GL_COLOR_BUFFER_BIT GL11/GL_DEPTH_BUFFER_BIT))
  (GL40/glPatchParameteri GL40/GL_PATCH_VERTICES 4)
  (GL11/glDrawElements GL40/GL_PATCHES 4 GL11/GL_UNSIGNED_INT 0)
  (Display/update)
  (Thread/sleep 40))

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

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

(GL15/glBindBuffer GL15/GL_ELEMENT_ARRAY_BUFFER 0)
(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)

(Display/destroy)

image

Any feedback, comments, and suggestions are welcome.

Enjoy!

Minimal OpenGL example in Clojure

Two years ago I published a minimal OpenGL example in C. Recently I got renewed interest in the Clojure programming language (if you want to learn Clojure, I can recommend the book Clojure in Action by the way). I hope to do some game development in Clojure. Initially I tried JOGL (Java Bindings for OpenGL), but then I switched to LWJGL version 2 (Lightweight Java Game Library). There is a Debian package for LWJGL version 2 and there is extensive documentation on the LWJGL Wiki. Using this library, I have translated my earlier example to Clojure. See code below:

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

(def vertex-source "#version 130
in mediump vec3 point;
in mediump vec2 texcoord;
out mediump vec2 UV;
void main()
{
  gl_Position = vec4(point, 1);
  UV = texcoord;
}")

(def fragment-source "#version 130
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]))

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

(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))))
    shader))

(defn make-program [vertex-shader fragment-shader]
  (let [program (GL20/glCreateProgram)]
    (GL20/glAttachShader program vertex-shader)
    (GL20/glAttachShader program fragment-shader)
    (GL20/glLinkProgram program)
    (if (zero? (GL20/glGetShaderi program GL20/GL_LINK_STATUS))
      (throw (Exception. (GL20/glGetShaderInfoLog program 1024))))
    program))

(defmacro def-make-buffer [method create-buffer]
  `(defn ~method [data#]
     (let [buffer# (~create-buffer (count data#))]
       (.put buffer# data#)
       (.flip buffer#)
       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))
(Display/create)

(def vertex-shader (make-shader vertex-source GL20/GL_VERTEX_SHADER))
(def fragment-shader (make-shader fragment-source GL20/GL_FRAGMENT_SHADER))
(def program (make-program vertex-shader fragment-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)
(GL11/glTexParameteri GL11/GL_TEXTURE_2D GL11/GL_TEXTURE_WRAP_S GL11/GL_REPEAT)
(GL11/glTexParameteri GL11/GL_TEXTURE_2D GL11/GL_TEXTURE_MIN_FILTER GL11/GL_NEAREST)
(GL11/glTexParameteri GL11/GL_TEXTURE_2D GL11/GL_TEXTURE_MAG_FILTER GL11/GL_NEAREST)
(GL30/glGenerateMipmap GL11/GL_TEXTURE_2D)

(GL11/glEnable GL11/GL_DEPTH_TEST)

(while (not (Display/isCloseRequested))
  (GL11/glClearColor 0.0 0.0 0.0 0.0)
  (GL11/glClear (bit-or GL11/GL_COLOR_BUFFER_BIT GL11/GL_DEPTH_BUFFER_BIT))
  (GL11/glDrawElements GL11/GL_TRIANGLES 3 GL11/GL_UNSIGNED_INT 0)
  (Display/update)
  (Thread/sleep 40))

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

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

(GL15/glBindBuffer GL15/GL_ELEMENT_ARRAY_BUFFER 0)
(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)

(Display/destroy)

You can run the code as follows:

sudo apt-get install liblwjgl-java clojure
wget http://www.wedesoft.de/downloads/raw-opengl.clj
clojure -cp /usr/share/java/lwjgl.jar raw-opengl.clj

image

Any feedback, comments, and suggestions are welcome.

Enjoy!

The MealMaster file format

The MealMaster file format (*.MMF or *.MM file) is a popular file format for sharing recipes. MealMaster is the file format introduced by the MealMaster software developed by Episoft Systems.

The website of Episoft Systems, which provided the original MealMaster software for DOS, is not online any more. Fortunately a copy of the website is still available on the Internet Archive.

One-column format

Below follows an example of the one-column MealMaster format (the ingredients are listed in a single column).

MMMMM----------------Meal-Master recipe exported by AnyMeal-----------------
     Title: Fruit-Meringue-Pie
Categories: pastries,cakes
  Servings: 1 pie

MMMMM----------------------------sponge mixture-----------------------------
    150 g  soft butter
    150 g  sugar
      3    eggs
    150 g  flour
      1 ts baking powder
MMMMM-------------------------------meringue--------------------------------
      4    egg white
    150 g  sugar
     50 g  almond leaves
MMMMM-------------------------------filling---------------------------------
    370 g  sour cherries;dripped off (
           -1 glass)
  1 1/2 tb cornflour
    1/4 l  cherry juice
    250 g  cream
      1 tb sugar
      1 pk stabiliser

Butter spring form with butter paper. Preheat oven to 200°.

MMMMM----------------------------sponge mixture-----------------------------
Stir butter, sugar and eggs until foamy, mix flour and baking powder and stir
in. Fill dough in in two spring forms and smooth down. Pre-bake for 15
minutes.

MMMMM-------------------------------meringue--------------------------------
Beat egg white until stiff and let sugar trickle in spoon by spoon. Beat
until stiff. Spread meringue base on pre-backed flan bases. Scatter almond
leaves over them. Bake for further 10-15 minutes. Let flan bases cool down on
a grid.

MMMMM-------------------------------filling---------------------------------
For the filling mix cornflour with some cherry juice. Bring remaining juice
to boiling point, stir in cornflour and boil up. Mix in sour cherries, let
cool down a bit and spread over one flan base.

Beat cream with sugar and stabiliser until stiff, spread over filling.
Cut second flan base in 12 pieces and reassemble on top of the filling. Let
cool down well.

MMMMM

The recipe header begins with MMMMM or -----. The header also has to contain the word "Meal-Master". The recipe content then starts with a title of up to 60 characters. Then a list of categories follows. Each category can have up to 11 characters. Then servings amount and unit are specified.

In this recipe there are three ingredient sections. An ingredient section starts with MMMMM or -----. Ingredient amounts are specified in column 1 to 7. Column 9 and 10 specify the unit token. Starting from column 12 ingredient text of up to 28 characters follows. If the ingredient text is longer than 28 characters, it is broken up using ingredient continuation lines. Ingredient continuation lines begin with a "-" in column 12.

Finally the instruction text follows. The instruction text also can be split up into multiple sections starting with MMMMM or -----. The recipe ends with MMMMM.

Two-column format

Here is an example of the two-column format. The ingredients are listed in two columns.

---------- Recipe via Meal-Master (tm) v8.01

      Title: Cannoli
 Categories: Italian, Desserts
      Yield: 16 servings

----------------------------------FILLING----------------------------------
  1 1/2 c  Whole-milk ricotta cheese;      1 1/2 c  Milk chocolate;
           - well drained                           - coarsely chopped
      3 tb Sugar                             1/4 c  Pistachio nuts;
  1 1/2 ts Cinnamon                                 - coarsely chopped

-----------------------------------DOUGH-----------------------------------
      1 c  All-purpose flour                        - or dry white wine
      1 tb Sugar                               2 c  Vegetable oil
      1 tb Butter or lard                           Colored sprinkles
      4 tb To 5 Tbl sweet Marsala wine

  In a bowl, combine all the filling ingredients and mix well. Refreigerate,
  covered, until ready to fill the cannoli shells.

  To make the dough, place the flour in a bowl or food processor. Add the
  butter or lard and sugar and mix with a fork, or pulse, until the mixture
  resembles coarse meal. Slowly add the 1/4 cup of wine and shape the mixture
  into a ball; add a little more wine if the dough appears too dry. It should
  be soft but not sticky. Knead the dough on a floured surface until smooth,
  about 10 minutes.  Wrap the dough and refrigerate for 45 minutes.

  Place the chilled dough on a floured work surface. Divide the dough in
  half.  Work with 1 piece of dough at a time; keep the remaining dough
  refrigerated. Roll the dough out to a very thin long rectangle about 14
  inches long and 3 inches wide, either by hand or using a pasta machine set
  to the finest setting.  Cut the dough into 3-inch squares. Place a cannoli
  form diagnoally across 1 square. Roll the dough up around the form so the
  points meet in the center.  Seal the points with a little water. Continue
  making cylinders until all the dough is used.

  In an electric skillet, heat the vegetable oil to 375F. Fry the cannoli 3
  or 4 at a time, turning them as they brown and blister, until golden brown
  on all sides. Drain them on brown paper. When they are cool enough to
  handle, carefully slide the cannoli off the forms.

  To serve, use a long iced tea spoon or a pastry bag without a tip to fill
  the cannoli with the ricotta cheese mixture. Dip the ends into colored
  sprinkles, arrange them on a tray, and sprinkle confectioner's sugar over
  the tops.  Serve at once.

  NOTE:  If you prefer, you can fry the cannoli in a deep fryer. Be sure to
  fill the cannoli just before serving - any sooner will make the shells
  soggy.

  This recipe from CIAO ITALIA by Mary Ann Esposito

-----

Here "Yield" is used instead of "Servings". Note that the order of ingredients in each section is column major. In this example the second ingredient section has a second column starting with an ingredient continuation line. I.e. it is continuing the last ingredient of the first column.

This example also shows instruction text being indented by two spaces. Empty lines are used to create paragraphs.

The recipe ends with -----.

Not shown in these examples is to use an instruction line beginning with a ":" to force a line break.

Units

The following table shows the meaning of each of the unit tokens:

special tokens
x "per serving" (quantity not to be scaled)
placeholder (no unit)
metric unit of volume
mlmilliliter (0.001 liter)
clcentiliter (0.01 liter)
dldeciliter (0.1 liter)
l liter
metric unit of weight
mgmilligram (0.001 gram)
cgcentigram (0.01 gram)
dgdecigram (0.1 gram)
g gram
kgkilogram (1000 gram)
U.S. unit of volume
flfluid ounce (1/128 gallon or 28.4130625 milliliter)
ptpint (1/8 gallon or 473.176473 milliliter)
qtquart (1/4 gallon or 0.946352946 liter)
gagallon (3.785411784 liter)
U.S. unit of weight
ozounce (1/16 pound or 28.34952312 gram)
lbpound (453.59237 gram)
unprecise unit of volume
drdrop (1/64 teaspoon)
dsdash (1/8 teaspoon)
pnpinch (1/16 teaspoon)
tsteaspoon (1/6 fluid ounce or 4.928921595 milliliter)
tbtablespoon (1/2 fluid ounce or 14.7867647825 milliliter)
c cup (8 fluid ounces or 236.5882365 milliliter)
unprecise description of size
smsmall
mdmedium
lglarge
unprecise description of quantity
cncan
pkpackage
ctcarton
slslice
bnbunch
alternate tokens
ea"each" (deprecated placeholder)
cbcubic centimeter (1 milliliter)
t teaspoon
T tablespoon

See Wikipedia page on approximate measures for more details.

Grammar

Here is an ABNF grammar of the MealMaster file format:

Mealmaster   = recipe Mealmaster
               / *( VCHAR / SP ) eol Mealmaster
               /
recipe       = header title categories servings ingredients instructions footer
header       = separator *( VCHAR / SP ) "Meal-Master" *( VCHAR / SP ) eol
title        = '     Title: ' 0*60( VCHAR / SP ) eol
categories   = 'Categories: ' categlist eol
categlist    = 1*11( VCHAR / SP ) ["," categlist]
servings     = '  Servings: ' 1*4( DIGIT ) [ SP [ 1*10( VCHAR / SP ) ] ] eol
ingredients  = onecolumn
               / twocolumn
onecolumn    = section onecolumn
               / ingredone eol onecolumn
               / *( SP ) eol onecolumn
               /
twocolumn    = section twocolumn
               / ingredtwo SP ingredone eol twocolumn
               / ingredone eol [ *( eol ) section twocolumn ]
               /
ingredone    = amount SP unit SP 1*28( VCHAR / SP )
ingredtwo    = amount SP unit SP 28( VCHAR / SP )
amount       = 7( SP / DIGIT / "." / "/" )
unit         = 'x ' / 'sm' / 'md' / 'lg' / 'cn' / 'pk' / 'pn' / 'dr' / 'ds'
               / 'ct' / 'bn' / 'sl' / 'ea' / 't ' / 'ts' / 'T ' / 'tb' / 'fl'
               / 'c ' / 'pt' / 'qt' / 'ga' / 'oz' / 'lb' / 'ml' / 'cb' / 'cl'
               / 'dl' / 'l ' / 'mg' / 'cg' / 'dg' / 'g ' / 'kg' / '  '
instructions = 0*255( VCHAR ) eol instructions
               / section instructions
               /
section      = separator *( VCHAR / SP ) eol
footer       = separator eol
separator    = 'MMMMM'
               / '-----'
eol          = [ CR ] LF

The grammar uses several rules from the ABNF standard:

  • / is used to specify alternative rules.
  • VCHAR stands for any printable character (except whitespace)
  • DIGIT is a digit (one of 0,1,2,3,...,9).
  • SP is the space character.
  • CR means carriage return (0x0D).
  • LF is the line-feed character (0x0A).
  • *( VCHAR / SP ) means any number of printable and space characters or no characters at all.
  • 1*4( DIGIT ) is used to specify one to four digits.
  • [ CR ] is an optional carriage return character.
  • 5( 'M' ) is exactly 5 capital "M"-characters.

Conclusion

The MealMaster file format is quite difficult to parse. Also there are a few problems with the file format:

  • It is not possible to protect a comma in a category-name from interpretation as a separator.
  • To prevent an occurrence of -----, : or MMMMM in the instructions-text from being interpreted as a special token, some recipe applications prepend all instruction lines with two space characters, which need to be ignored during import.
  • The original Mealmaster software will refuse to import recipes exceeding a certain size. Also overlong titles, categories, and servings will be cropped.
  • There are many recipes with ingredient-text longer than 28 characters. Overlong ingredient lines are making it more difficult to distinguish between the one- and two-column format.
  • The ending of the ingredient section is not defined properly.
  • Some recipe applications are relying on an empty line separating the ingredients from the instructions. On the other hand the original Mealmaster application allows empty lines within the ingredient-section.
  • There are recipe applications exporting MealMaster files containing categories with more than 11 characters.

I hope this description will be useful to you if you are trying to parse MealMaster recipes. Please let me know any feedback in the comment section below.

New revamped version of the AnyMeal recipe management software

More than 15 years ago after leaving university I developed a GNU/Linux software called AnyMeal for managing a large amount of recipes. The software was based on the MealMaster file format which is a popular file format for sharing recipes.

The website of Episoft Systems, which provided the original MealMaster software for DOS, is not online any more. Fortunately a copy of the website is still available on the Internet Archive.

The AnyMeal software used the recode library to convert recipe text files to the UTF-8 encoding. A fast Flex parser then converted the MealMaster recipes to XML. An XSLT script was used to generate SQL commands for inserting the recipe into a MySQL database. For setting up the database and user account, the software had a step-by-step wizard. The KDE password manager was used to store the user credentials.

The recipe search was performed using a complicated search dialog with multiple tabs. The graphical user interface used multiple windows to display recipes and lists of search results. A recipe editor was implemented for modifying existing recipes or adding new ones.

However life happened and eventually I stopped maintaining the software. The software was developed using Qt3 and when Qt4 came out, there were breaking changes with list views and the software didn't work on new systems any more.

Now many years later it still looks like there is a space for a fast recipe management software which can handle a large amount of MealMaster recipes. So I decided to use my past experience and do a rewrite of the old software.

This time I used the Qt5 framework to implement the graphical user interface. Instead of the old list view items, Qt5 now uses an item model and a list view widget to view the model. Also Qt5 provides cross-platform support for printing which is a great feature for a recipe management software. Like that it is not necessary to have a computer in the kitchen any more ;)

Instead of MySQL, I used the SQLite3 embedded database. This greatly simplified the software because SQLite3 does not require the setup of user accounts and databases. Also the user does not have to run a database server in the background. Furthermore without user credentials there is no need for interfacing with the KDE password manager any more.

For the internal representation of recipes I decided to use a more conservative approach. For representing recipes I used a C++ class instead of XML data. Like that it is not necessary to depend on the Xalan XSLT processor and Xerces-C XML library any more.

I also changed the order of UTF-8 conversion and parsing of recipes. Now the recipes are first parsed with an Flex MealMaster parser and stored as C++ objects. The strings of the C++ object then get converted to UTF-8 using the recode library. Like that it is not necessary for the Flex parser to be UTF-8 aware any more thus simplifying the implementation.

The search was also simplified. A search can be performed on title, category, or ingredient. It is possible to narrow down the search result by using several searches one by one. I.e. it is not necessary any more to implement a complicated search dialog which builds a complex search query.

Back then I used CVS for version control. This time the version control was done using Git. AnyMeal is still hosted on SourceForge however the source repository is now on Github, Gitlab, and BitBucket (after all Git is a distributed version control system!). The main project page is on Github (which displays the content of the gh-pages branch of the repository).

After simplifying the software architecture like that, it was possible to implement the software in less than 100 hours. Most of the work of the rewrite was implementing a simpler Flex parser and reimplementing the recipe editor. In contrast to back then, I implemented the internals of the application using test-driven development (TDD). I used Google Test which is Google's C++ test framework. Also, unlike back then, now there is Travis CI which provides free automated testing for software libre.

Finally using MSYS and MinGW the software was also compiled for Microsoft Windows. The Nullsoft Scriptable Install System (NSIS) was used to create an installer.

I am planning to add a few more small features but I think the software is more or less ready to be used for cooking.

Let me know what you think in the comment section below.

Enjoy!

Update:

Article was submitted to Hacker News.

I also forgot to mention that I used Docker and debuild to create a package for Debian Sid.

Rigid body game physics 6

part 1 part 2 part 3 part 4 part 5 part 6

A realistic simulation requires the estimation of friction forces.

Friction Forces

Colliding as well as resting contacts cause constraint impulses as well as friction impulses. So far we have only estimated the constraint impulses of colliding and resting contacts. Constraint impulses are in the direction of the contact's normal vector. Friction impulses act tangential to the contact plane (orthogonal to the normal vector). The inequality constraint is J and b have two rows. The vectors t1 and t2 are chosen so that they are orthogonal to the normal vector n and orthogonal to each other. Friction forces try to reduce tangential velocities to zero. I.e. b is

The linear components of J are and the angular components of J are

As before λ is computed Here λ has two elements. λ1 and λ2 are scaled with the same positive factor if necessary so that the length of the 2D vector consisting of λ1 and λ2 is smaller than the normal impulse Pn multiplied with the friction constant μ. Basically the overall force vector needs to reside in the friction cone. In a similar fashion as for constraint impulses, friction impulses are subtracted, recomputed, and then added to the accumulated impulses P of the two objects, when iterating.

friction cone

If you have made it this far, you know how to build a small physics engine!

Any feedback, comments, and suggestions are welcome.

dropping box

Further reading