% language=us runpath=texruns:manuals/luametafun

\environment luametafun-style

\startcomponent luametafun-threed

\startchapter[title={Three dimensional surfaces}]

{\em todo: maybe a few word how eye and such relate to positions}

This is {\em not} a chapter that goes in depth into how to render 3D images in
\LUAMETAFUN, because for that we have a dedicated manual; there are just too many
commands and parameters involved. Here we just discuss some basics. The rendering
is centered around meshes, These are just a list of triangles coded as triplets.
Each of the three entries is an index into a points list. You can read more about
meshes in the chapter on contour graphics (vectors). In most cases we only need a
list of points (vertices) and can delegate dealing with triangle mapping to the
engine, a probably somewhat uncommon approach but actually quite efficient; why
bother with triangles when there is no need for.

\startbuffer[a]
\startluacode
    metapost.registermesher("demo", function(specification)
        return {
            name      = "demo",
            id        = specification.id,
            vertices  = {
                { 1, 1, 1 }, { 3, 1, 2 },
                { 3, 3, 3 }, { 1, 3, 3 },
            },
            -- alternative 1:
            triangles = {
                { 1, 2, 3 }, { 1, 3, 4 },
            },
            -- alternative 2:
            meshtype  = 7,
        }
    end)
\stopluacode
\stopbuffer

\typebuffer[a][option=TEX] \getbuffer[a]

Here we have a quad (four corners) split into two triangles. Two vertices are
shared. Instead of providing the in this case trivial list of triangles you can
specify a meshtype. An alterative way to define this is:

\startbuffer[b]
\startluacode
    metapost.registermesher("demo", function(specification)
        return {
            name      = "demo",
            id        = specification.id,
            vertices  = {
                { 1, 1, 1 }, { 3, 1, 2 }, { 3, 3, 3 },
                { 1, 1, 1 }, { 3, 3, 3 }, { 1, 3, 3 },
            },
            -- alternative 1:
            triangles = {
                { 1, 2, 3 }, { 4, 5, 6 },
            },
            -- alternative 2:
            meshtype  = 5,
        }
    end)
\stopluacode
\stopbuffer

\typebuffer[b][option=TEX]

% \getbuffer[b]

Before we move on, let's stress that this manually defining a mesh is not what
this feature is about, it is just one of the ways to provide a mesh. More common
are parametric and implicit surfaces, planes and overlapping areas (or curves) as
well as external meshes generated by other programs. These are discussed in that
other manual!

The following code will render this mesh. We set up a scene, define material and
then register the internal mesh. Next we render so that we know the scenery which
is needed in order to draw on top of what gets generated: a bytemap (see
elsewhere for that that is about).

The collected meshes are put on a canvas in order and hidden surfaces are kept
hidden, although with opacity enabled one can see them. In the end one gets a
bytemap of given resolution with anti-aliasing with a quality driven by the
supersampling.

\startbuffer
\startMPcode
    lmt_scene_start [
        bytemap         = 34,
        width           = 5cm,
        crop            = true,
        supersample     = 4,
        projection      = "perspective",
        eye             = (-2,-2,1),
        target          = (0,2,1),
        backgroundcolor = "darkblue", % (0,0,1)
    ] ;

    lmt_scene_material [
        name      = "surface",
        diffuse   = "middlegray", % or resolvedcolor("middlegray") or (1,1,0)
    ] ;

    lmt_scene_internal [
        id       = "one",
        name     = "demo",
        material = "surface",
    ] ;

    lmt_scene_render ;

    pickup pencircle scaled 1 ;

    draw lmt_scene_point(1,1,1) -- lmt_scene_point(3,3,3) withcolor "darkred" ;

    pickup pencircle scaled 10 ;

    drawdot lmt_scene_point(1,1,1) withcolor "darkred"   ; % common
    drawdot lmt_scene_point(3,1,2) withcolor "darkgreen" ;
    drawdot lmt_scene_point(3,3,3) withcolor "darkred"   ; % common
    drawdot lmt_scene_point(1,3,3) withcolor "darkgreen" ;

    setbounds currentpicture to lmt_scene_bounds ;

    lmt_scene_stop ;
\stopMPcode
\stopbuffer

\typebuffer[option=TEX]

This setup results in the following rendering where the red points are the shared
ones:

\startlinecorrection
    \getbuffer
\stoplinecorrection

Drawing the points can be tricky because they get projected so the points in the
mesh are not the points we get. The eye and target need to be set accordingly.

Constructing a mesh is normally done with some program and one can either produce
\LUA\ tables or vertex and mesh userdata objects. We just show these alternatives:

\starttyping[option=TEX]
\startluacode
    metapost.registermesher("demo", function(specification)
        local vertices  = vector.points.new(4)
        local triangles = vector.mesh.new(2)
        vertices(1,1,1)
        vertices(3,1,2)
        vertices(3,3,3)
        vertices(1,3,3)
        triangles(1,2,3)
        triangles(1,3,4)
        return {
            name      = "demo",
            id        = specification.id,
            vertices  = vertices,
            triangles = triangles,
        }
    end)
\stopluacode
\stoptyping

Here we explicitly generate vertex and triangle data, but we could in this case
have used a simple triangle setup:

\starttyping[option=TEX]
\startluacode
    metapost.registermesher("demo", function(specification)
        local vertices  = vector.points.new(4)
        local triangles = vector.mesh.new(2,7)
        vertices(1,1,1)
        vertices(3,1,2)
        vertices(3,3,3)
        vertices(1,3,3)
        return {
            name      = "demo",
            id        = specification.id,
            vertices  = vertices,
            triangles = triangles,
        }
    end)
\stopluacode
\stoptyping

We even could have said:

\starttyping[option=TEX]
\startluacode
    metapost.registermesher("demo", function(specification)
        local vertices = vector.points.new(4)
        vertices(1,1,1)
        vertices(3,1,2)
        vertices(3,3,3)
        vertices(1,3,3)
        return {
            name      = "demo",
            id        = specification.id,
            vertices  = vertices,
            meshtype  = 7,
        }
    end)
\stopluacode
\stoptyping

We have a few predefined meshes for users to play with and that can serve as
example of how to create your own library. because specifications get passed, one
can act accordingly, here for instance \type {level} is set to 2. The result is
shown in \in {figure} [fig:mengersponge].

\startbuffer
\useMPlibrary[meshes-mengersponge]

\startMPcode
    lmt_scene_start [
        bytemap      = 34,
        width        = 6cm,
        crop         = true,
        resolution   = 300,
        supersample  = 2,
        projection   = "perspective",
        eye          = (20,20,20),
        target       = (5,5,5),
        up           = (0,0,1),
        light        = (-1.2,-1,-2),
        intensity    = 1,
        %
        cache        = true,
    ] ;

    lmt_scene_material [
        name      = "surface",
        diffuse   = (0,0.5,.5),
        specular  = (.40,.40,.40),
        shininess = 40,
     %  opacity   = .15,
    ] ;

    lmt_scene_internal [
        id       = "one",
        name     = "mengersponge",
        level    = 2,
        size     = 10,
        material = "surface",
    ] ;

    lmt_scene_stop ;

    copybytemap 34 to 35 ; reducebytemap 35 ;

    draw (bytemap 35 xsized 6cm) shifted (7cm,0) withbytemasked 255 ;
\stopMPcode
\stopbuffer

\typebuffer[option=TEX]

This is a nice test case for code generating \LLM's because as of mid 2026 most
seem to get it wrong. It looks like they all base solutions in the same code
harvested somewhere on the web, sometimes taking a bit of freedom to translate
from whatever original language into \LUA. The solution we used partially came
from the \AI\ feature in Google Chrome, given that one invites it to also add the
{\em right} normals. These are simple experiments and errors are harmless because
one does a visual check in the end.

\startplacefigure
    [reference=fig:mengersponge,
     title={A level~2 Menger Sponge. The rightmost variant is a gray scale copy.},
     location=top]
    \framed
      [offset=.5ts,strut=no,frame=off,
       background=color,
       backgroundcolor=lightgray]
      {\getbuffer}
\stopplacefigure

In this example we also demonstrate that because the result in in a bytemap you
can mess around with that afterwards. Watch how we define a mask so that the
image can be put on a background with that color.

As an experiment added support for processing the intermediate result and this is
how it's done:

\startbuffer
\startluacode
    local random = math.random

    local function process(r,g,b)
        return
            5 * random(0,1) * r,
            5 * random(0,1) * g,
            5 * random(0,1) * b
    end

    metapost.registermeshupper {
        name    = "demo",
        version = 1.000, -- makes caching unique
        skip    = true,  -- only when no background
        color   = true,  -- red, green, blue
        process = process,
    }
\stopluacode
\stopbuffer

\typebuffer[option=TEX] \getbuffer

In order for this work we need to add a \type {process} subtable to the scene
specification. The result of this can be seen in \in {figure} [fig:mengermeshup].

\startbuffer
\startMPcode
    lmt_scene_start [
        bytemap      = 34,
        width        = 6cm,
        crop         = true,
        resolution   = 300,
        supersample  = 2,
        projection   = "perspective",
        eye          = (20,20,20),
        target       = (5,5,5),
        up           = (0,0,1),
        light        = (-1.2,-1,-2),
        intensity    = 1,
        process      = "demo",
      % cache        = true,
    ] ;

    lmt_scene_material [
        name      = "surface",
        diffuse   = (.25,0,.25),
        specular  = (.40,.40,.40),
        shininess = 40,
    ] ;

    lmt_scene_internal [
        id       = "one",
        name     = "mengersponge",
        level    = 2,
        size     = 10,
        material = "surface",
    ] ;

    lmt_scene_stop ;
\stopMPcode
\stopbuffer

\typebuffer[option=TEX]

\startplacefigure
    [reference=fig:mengermeshup,
     title={A Menger Sponge with the \type {demo} meshupper applied.},
     location=top]
    \framed
      [offset=.5ts,strut=no,frame=off,
       background=color,
       backgroundcolor=lightgray]
      {\getbuffer}
\stopplacefigure

% register
%
%     metapost.registermeshupper( "demo-2",firsthack,"skip,"color)

\startbuffer[demo-1]
\startluacode
    local random = math.random

    local function process(r,g,b)
        return
            1.5 * random(0,1) * r,
            1.5 * random(0,1) * g,
            1.5 * random(0,1) * b
    end

    metapost.registermeshupper {
        name    = "demo",
        version = 1.000, -- makes caching unique
        skip    = true,  -- only when no background
        color   = true,  -- red, green, blue
        process = process,
    }
\stopluacode
\stopbuffer

\startbuffer[demo-2]
\startluacode
    local sin = math.sin
    local cos = math.cos

    local function process(r,g,b,x,y,z,t)
        local a = 1 - sin(x)
        local b = 1 - cos(y)
        local c = cos(z) + sin(z)
        return a * r, b * g, c * b
    end

    metapost.registermeshupper {
        name    = "demo",
        version = 1.000, -- makes caching unique
        skip    = true,  -- only when no background
        color   = true,  -- red, green, blue
        texture = true,  -- t, x, y, z
        process = process,
    }
\stopluacode
\stopbuffer

\startbuffer[demo-3]
\startluacode
    local sin = math.sin
    local cos = math.cos

    local function process(row,col,r,g,b)
        local f1 = sin(row/3) * cos(col/2)
        local f2 = cos(col/3) * cos(row/2)
        local f3 = cos(col/3) * sin(row/2)
        return f1 * r, f2 * g, f3 * b
    end

    metapost.registermeshupper {
        name    = "demo",
        version = 1.000, -- makes caching unique
        skip    = true,  -- only when no background
        slot    = true,  -- row, column
        color   = true,  -- red, green, blue
        process = process,
    }
\stopluacode
\stopbuffer

\startbuffer[demo]
\startMPcode
    lmt_scene_start [
        bytemap      = 34,
        width        = .5TextWidth,
        crop         = true,
        resolution   = 300,
      % resolution   = 150,
        supersample  = 2,
        projection   = "perspective",
        eye          = (20,20,20),
        target       = (5,5,5),
        up           = (0,0,1),
        light        = (-1.2,-1,-2),
        intensity    = 1,
        process      = "demo",
      % cache        = true,
    ] ;

    lmt_scene_material [
        name      = "surface",
        diffuse   = (.75,0.9,.55),
        specular  = (.40,.80,.30),
        shininess = 20,
        texture   = 1,
    ] ;

    lmt_scene_parametric [
        id       = "sphere",
        x        = "sin(u)*cos(v)",
        y        = "sin(u)*sin(v)",
        z        = "cos(u)",
        xu       = "cos(u)*cos(v)",
        yu       = "cos(u)*sin(v)",
        zu       = "-sin(u)",
        xv       = "-sin(u)*sin(v)",
        yv       = "sin(u)*cos(v)",
        zv       = "0",
        umin     = 0,
        umax     = pi,
        vmin     = 0,
        vmax     = 2*pi,
        nu       = 100,
        nv       = 100,
        material = "surface",
    ] ;

    lmt_scene_stop ;
\stopMPcode
\stopbuffer

The processors can act on properties of the graphic and the mechamism is such
that we can we can let it evolve over time. Say we have the following, where we
render at 300 dots per inch, calculate with twice that resolution as set by the
\type {supersample} key, and make sure we use the right target size so that
the bitmap doesn't need to be scaled afterwards.

\typebuffer[demo][option=TEX]

A simple processor that just messes with the color is the following; we already
saw a variant earlier. The result is shown \in {figure} [fig:threed:process:1].

\typebuffer[demo-1][option=TEX]

\startplacefigure
    [reference=fig:threed:process:1,
     title={The result of \type{demo} process 1: manipulating colors.}]
    \getbuffer[demo-1,demo]
\stopplacefigure

You can also operate on the coordinates which is demonstrated in \in {figure}
[fig:threed:process:2]. These are the \im {x}, \im{y} and \im {z} values before
projection.

\typebuffer[demo-2][option=TEX]

\startplacefigure
    [reference=fig:threed:process:2,
     title={The result of \type{demo} process 2: manipulating colors using coordinates.}]
    \getbuffer[demo-2,demo]
\stopplacefigure

A third variant operates on the rows and columns of the \quote {final} image. \in
{Figure} [fig:threed:process:3] shows what we get.

\typebuffer[demo-3][option=TEX]

\startplacefigure
    [reference=fig:threed:process:3,
     title={The result of \type{demo} process 3: manipulating colors using the final pixel positions.}]
    \getbuffer[demo-3,demo]
\stopplacefigure

Currently you can ask for the following properties. When the corresponding key is
\type {true} you will get these, in the following order:

\starttabulate[|T|T|p|]
\NC slot    \NC row column     \NC positive integers   \NC \NR
\NC color   \NC red green blue \NC positive numbers    \NC \NR
\NC normal  \NC nx ny nz       \NC numbers             \NC \NR
\NC texture \NC x y z t        \NC numbers and integer \NC \NR
\stoptabulate

The texture might eventually become more advanced and the \type {t} specifier is
just an integer that at some point can get more meaning. For instance for
parametric curves we might also provide so called \im {u} and \im {v} coordinates
combined with specific options triggered by this \type {t}.

{\em Here we will add some of the more advanced examples that Keith made of
textures that also will become (example) plug-ins.}

We hope that this teaser will trigger your curiosity to read the more extensive
manual on this topic. We anyway end with a warning: rendering these graphics can
use a lot of memory but we try to clean up when we can. The final results are
bitmaps and therefore reasonable in size. You might cache the images using one of
the various possibilities present in \CONTEXT. The width, resolution and
supersampling all add to the memory usage. A 6~cm wide figure, at 300 dots per
inch and level~2 supersampling can end up between 500~MB and 1~GB which is still
okay given what one asks for. The Menger Sponge in this chapter takes some 160~MB
before the bytemap is created and has 1420 times 1420 pixels. We can dial down a
bit by compiling the colors using floats instead of doubles but we keep the rest
double so the gain is relatively small.

You can speed up a run by setting \type {cache} to \type {true}. The settings as
well as bytemap are then saved and reused. When a setting changes, a new bytemap
is generated. The runtime files can be removed with \typ {context --purge}.

As a teaser for the more advanced manual, we show an example of what gets
discussed there. If you watch \in {figure} [fig:threedteaser] you see how we
start a scene, define a plane (with material), an implicit shape (also with its
material) and finally an intersection of these two shapes. We then render the
image so that its properties are known, for instance the normal vector that we
draw on top.

\startbuffer[K3]
\startMPcode
lmt_scene_start [
    width        = 10cm,
    resolution   = 300,
    supersample  = 2,
    crop         = true,
    projection   = "perspective",
    eye          = (-8,32,0),
    fov          = 80,
    light        = (0,-1,-2),
    intensity    = 1.1,
    cache        = true,
    name         = "example",
] ;

lmt_scene_material [
    name      = "plane",
    diffuse   = (.3,.6,.6),
    specular  = (.25,.25,.25),
    shininess = 18,
    opacity   = 0.5,
] ;

lmt_scene_plane [
    id       = "plane",
    material = "plane",
    center   = (1,0,0),
    normal   = (1,0,0),
    usize    = 8.75,
    vsize    = 8.5,
] ;

lmt_scene_material [
    name      = "K3",
    diffuse   = (.9,.2,.2),
    specular  = (.5,.5,.5),
    shininess = 25,
    opacity   = 1,
    ambient   = .25,
] ;

lmt_scene_implicit [
    id       = "K3",
    material = "K3",
    code     = "(1+x*x)*(1+y*y)*(1+z*z)-16*x*y*z-4",
    dfdx     = "2*x*(1+y*y)*(1+z*z)-16*y*z",
    dfdy     = "2*y*(1+x*x)*(1+z*z)-16*x*z",
    dfdz     = "2*z*(1+x*x)*(1+y*y)-16*x*y",
    xmin     = -5,
    xmax     =  5,
    ymin     = -5,
    ymax     =  5,
    zmin     = -5,
    zmax     =  5,
    nx       = 150,
    ny       = 150,
    nz       = 150,
    normal   = "smooth",
] ;

lmt_scene_material [
    name      = "intersection",
    diffuse   = (0,0,0),
    diffuse   = (0,0,1),
    specular  = (.25,.25,.25),
    shininess = 18,
    opacity   = 1,
    dx        = 10,
    dy        = 10,
] ;

lmt_scene_intersection [
    material       = "intersection",
    first          = "K3",
    second         = "plane",
] ;

lmt_scene_render ;

% triplet normal ; normal = lmt_scene_calculate_normal("K3",(0,1,1));

% pair p ; p := lmt_scene_project_point (0,1,1) ;
% pair q ; q := lmt_scene_project_point normal  ;

% drawdot  p withpen pencircle scaled 2mm
%     withcolor white ;

% drawarrow p -- (p shifted .1q)
%     withpen pencircle scaled .5mm
%     withcolor white ;

draw scenenormal "K3" (0,1,1)
    withpen pencircle scaled 1pt
    withcolor "white"
;
draw scenepoint "K3" (0,1,1)
    withpen pencircle scaled 3pt
    withcolor "white"
;

lmt_scene_stop ;
\stopMPcode
\stopbuffer

\startplacefigure[reference=fig:threedteaser,title=Just a teaser.]
    \getbuffer[K3]
\stopplacefigure

\typebuffer[K3][option=TEX]

When you run that example the log will show something like this (on my 2018
laptop):

\starttyping[style=small]
metapost 3D     > plane mesh: 4 vertices, 2 triangles, runtime 0.000
metapost 3D     > implicit mesh: 937620 vertices, 312540 triangles, runtime 1.918
metapost 3D     > mesh plane: 1752 segments, 1 chains, runtime 0.204
metapost 3D     > unprojected dimensions: x [-4.012511376 4.012773906], y [-4.375 4.375], width 8.025285281, height 8.75
metapost 3D     > zbuffer width 2364, height 2578, supersample 2
metapost 3D     > 1 1 : delayed   : plane, opacity 0.500, id 'plane'
metapost 3D     > 1 2 : triangles : implicit, opacity 1.000, id 'K3', runtime 0.456
metapost 3D     > 1 3 : points    : points, opacity 1.000, id 'K3 plane', method 0, runtime 0.013
metapost 3D     > 2 1 : triangles : plane, opacity 0.500, id 'plane', runtime 0.088
metapost 3D     > composing: max levels 1, runtime 0.056
metapost 3D     > cropping: xmin 147, ymin 10, xmax 1141, ymax 1280, width 995, height 1271, runtime 0.030
metapost 3D     > trying to clean up 1.054.573.448 bytes of memory
\stoptyping

Assuming a more than twice as fast modern machine, these timings are quite okay
but nevertheless we cache the image. The embedded image is less than a few
hundred K, so that's not too bad, but you will notice that quite some memory is
used to get there. This is a result of keeping quite a bit of dat ain \LUA, the
use of double precision floating numbers, three dimensional meshes that can be
covering others, and other temporary data stores. We keep track of three
dimensions, normals, textures, etc. We might decide to compile \LUAMETATEX\ with
single precision floats but for now we stick to the default. We clean up memory
in-between because \LUA's garbage collector will not kick in for the relative
small objects, even if there are many. Therefore it is no problem to generate
many hundreds of pages with such images when (like Keith) you make animations.

You can, it you like, also load a so called \type {stl} file, for instance
produced by Mathematica or Maple. So, if the above doesn't inspire you, maybe the
next example does: \in {figure} [fig:threedalsoteaser]. In this case it's an
\type {stl} file exported from \CONTEXT.

\startbuffer
\startMPcode
lmt_scene_start [
    width        = 10cm,
    resolution   = 300,
    supersample  = 2,
    crop         = true,
    projection   = "perspective",
    eye          = "auto",
    target       = "auto",
    fov          = 80,
    light        = (0,-1,-2),
    intensity    = 1.1,
  % cache        = true,
  % name         = "example",
] ;

lmt_scene_material [
    name      = "surface",
    diffuse   = (.8,0,0),
    specular  = (1,.5,0),
] ;

lmt_scene_external [
    filename    = "luametafun-threed-sphere.stl",
  % flipnormals = true,
  % normal      = "flat",
    id          = "sphere",
    material    = "surface",
    vector      = true,
] ;

lmt_scene_render ;

draw lmt_scene_mesh "sphere"
    withpen pencircle scaled 1pt withcolor white ;
;

lmt_scene_stop ;
\stopMPcode
\stopbuffer

In this example we show the mesh which can be handy when you want to check it all
is right, but in practice you will never do that other than that because you get
the whole mesh (also the hidden pieces) and it bloats the file. We've ran tests
with exported \type {stl} files and one will be surprised by how many triangles
are involved. Also, keep in mind that these files can have normals pointing the
other way so you might want to flip them. Forcing flat normals is a way out. The
\type {auto} flag tells \CONTEXT\ to come up with a reasonable \type {eye} for
imported files.

\startplacefigure[reference=fig:threedalsoteaser,title={Another teaser: rendering an \type {stl} file.}]
    \getbuffer
\stopplacefigure

\typebuffer[option=TEX]

\stopchapter

\stopcomponent

% For Keith's animations:
%
% scenery = [
%     filename    = "noisy-k3.png",
%     nofframes   = \Frames,
%     loop        = 0, % 1
%     numerator   = 15,
%     demominator = 100,
% ]
