== Lesson 23: Models and Light image::learn/23-models.png[The finished lesson, 480] Boxes and spheres will take you a long way, but sooner or later the game wants a dragon. A *model* is a shape somebody else built in a modeling program: hundreds of triangles, its own colors and surfaces, sometimes a skeleton and movements to go with it, all in one file. Loading one is two lines. Getting it to look like anything is light, and light is most of this lesson. You need no artwork and no downloads. The first time you ever ran Singe it unpacked a folder called `Singe` into your work folder, and two models are sitting in it: `DragonModel.glb`, the dragon from the engine's own title screen, and `SingeText.glb`, the lettering underneath it. This lesson uses the dragon. === The Scene Around It A model needs somewhere to stand. Make a folder, make `dragon.singe` in it, and start with a floor, a light, and a camera. If you have done lesson twenty-two none of this is new; if you have not, it is a scene turned on, a flat slab to stand on, one sun, and a point to look from. [source,lua] ---- sceneEnable(true) sceneSetBackground(10, 12, 20) sceneSetAmbient(28, 30, 38) local stone = materialNew() materialSetColor(stone, 105, 105, 115) materialSetRoughness(stone, 0.9) local floor = nodeNew() nodeSetMesh(floor, meshBox(24, 0.4, 24), stone) nodeSetPosition(floor, 0, -0.2, 0) local camera = nodeNew() nodeSetPosition(camera, 0, 2.6, 9) nodeLookAt(camera, 0, 1.7, 0) cameraSet(camera) cameraSetPerspective(55, 0.1, 200) function onOverlayUpdate() return OVERLAY_UPDATED end ---- Run it with `Singe -R dragon`. A gray slab, seen from slightly above, on a dark blue background. The slab is twenty-four units across and a little under half a unit thick, and sitting it at `y = -0.2` puts its top surface exactly at `y = 0`, which makes everything you place later easy to reason about: the floor is zero. === Loading the Dragon Two lines. [source,lua] ---- local dragonModel = modelLoad("Singe/DragonModel.glb") local dragon = modelInstance(dragonModel) ---- Put them after the floor and save. A dragon appears, and it is far too big for the picture -- you are looking at its middle. === What Just Happened The two lines do different jobs, and the difference matters. `modelLoad` reads the file, hands the shapes and their surfaces to the graphics card, and keeps the whole thing ready. It is the slow call. Do it once, when the game starts, and never in the middle of play. `modelInstance` places a copy in the scene and hands you back a node -- an ordinary node, the same kind `nodeNew` gives you, which you position, rotate, scale, and delete with the calls you already know. Instancing is the cheap call. Load one tree and instance it forty times and you have a wood, with one copy of the shapes on the graphics card and forty places they are drawn. What comes with the model is everything the file had in it: the shapes, the surfaces and their colors, the way the parts are arranged, any skeleton, and any movements. You do not build materials for a loaded model; it brought its own. === Scale, and the First Thing That Goes Wrong The dragon is about seven and a half units tall. Your camera is nine units back. That is why it fills the window. [source,lua] ---- nodeSetScale(dragon, 0.5) ---- Add that under the `modelInstance` line. Now it is under four units tall, stands on the floor, and fits the shot. This is not a detail; it is the commonest first problem with a model, and it has two faces. A model built in centimeters arrives a hundred times too big and you see one enormous surface filling the screen, which usually reads as "my scene is broken" rather than "I am inside the dragon's neck". A model built in some other unit arrives so small it is a dot, or invisible against the background, and you assume the file failed to load. The fix is always the same: a scale on the instance's node. Not on the parts, not in the modeling program if you can help it, but one `nodeSetScale` on the node `modelInstance` handed you, chosen by trying a number and looking. `0.01` and `100` are both perfectly ordinary answers. When you cannot tell how big something is, give yourself a ruler. Put a one-unit box beside it: [source,lua] ---- local ruler = nodeNew() nodeSetMesh(ruler, meshBox(1, 1, 1), stone) nodeSetPosition(ruler, 3, 0.5, 0) ---- A crate-sized object next to the model tells you in one glance whether the model is a dragon or a mountain. Take the ruler out when you are done. While you are placing it, turn it to face you: [source,lua] ---- nodeSetRotation(dragon, 0, -60, 0) ---- The dragon was modeled facing along positive X, which is to the right, so it starts in profile. Turning it about Y brings it round toward the camera. A model's "front" is whatever direction the person who built it chose, and finding out means turning it and looking. === Light Run what you have now. The dragon is there, and it is dreary: a flat, dim shape with no highlights and no shadow. That is because the only light in your scene is the ambient, the faint glow from everywhere that `sceneSetAmbient` sets. This is the single most common disappointment in 3D, and it happens to everyone: the model loaded perfectly and the scene is black, or nearly so, because nothing is shining on it. Before you suspect the file, put a light in. There are three kinds, and each is a node, so you place and aim them with `nodeSetPosition` and `nodeLookAt` like anything else. [source,lua] ---- local sun = lightNew(LIGHT_DIRECTIONAL) nodeSetPosition(sun, -6, 9, 7) nodeLookAt(sun, 0, 1.5, 0) lightSetColor(sun, 255, 244, 224) lightSetIntensity(sun, 1.8) lightSetShadow(sun, true) ---- `LIGHT_DIRECTIONAL` is a sun. It is treated as being infinitely far away, so only the direction it shines in counts and everything in the scene gets the same amount of it. Its position is there for `nodeLookAt` to aim it with. This is the light that gives a scene its main shape, and most scenes want exactly one. `lightSetIntensity` is brightness. For a directional light, `1` lights a white surface square-on to it up to white, so `1.8` is a bright day. `lightSetColor` tints it: a slightly warm white for sun, a cold blue for moonlight, and the reference's Light section for the rest. `lightSetShadow(sun, true)` is what makes the dragon sit on the floor rather than hover above it. Shadows are off by default because each shadow-casting light costs another pass over the whole scene every frame. Turn it on for the one light that matters and leave it off for the others; a small machine will thank you. [source,lua] ---- local torch = lightNew(LIGHT_POINT) nodeSetPosition(torch, 3.5, 1.2, 3.0) lightSetColor(torch, 255, 150, 70) lightSetIntensity(torch, 14) lightSetRange(torch, 12) ---- `LIGHT_POINT` is a bulb. It sits at a place, shines in every direction, and fades with distance. That fading is why the number is `14` and not `1`: the intensity of a point light is its brightness at the light itself, and by the time it has crossed a few units there is not much left. A bulb lighting a room usually wants something in the tens. If a point light seems to do nothing, that is almost always the reason. `lightSetRange` says how far it reaches before it stops entirely, which keeps a lamp in one room from faintly lighting the next one and costs nothing to set. The third kind is `LIGHT_SPOT`, a cone pointing down the node's own negative Z, with `lightSetCone` for the angle of the bright middle and the soft edge: a torch, a headlight, a searchlight. Up to eight lights shine at once, which is more than most scenes need. If you want more, you want fewer. Run it now. A near-white sun from the left, a warm orange glow from the right, a shadow on the slab, and a dragon with a front and a back. === Materials, at the Level You Need Today The dragon brought its own materials, and there are three of them: a dark metal, a paler champagne metal, and a pale glass you can see a little way into. Most of its parts are cut from more than one. You do not have to touch any of it, and most of the time you should not. When you do want to change how a part of a model looks, you make a material the way lesson twenty-two did and put it on the part: [source,lua] ---- local red = materialNew() materialSetColor(red, 220, 50, 40) materialSetRoughness(red, 0.4) nodeSetMaterial(nodeFind("crest", dragon), red) ---- `nodeFind(name, root)` searches a model instance for a part by the name it had in the file and hands back its node, or `nil` when there is no such name. That is how you reach inside a model: a turret, a wheel, a hand to hang a torch on, or the dragon's crest. The crest is one piece of one material, so its node carries the shape itself and the line above repaints it. A part built from several materials is kept as a node with one child per piece, and putting a material on the parent changes nothing you can see. When a `nodeSetMaterial` appears to do nothing, that is why, and `nodeGetChildren` will show you what is underneath. Four numbers describe most surfaces. The color, from `materialSetColor`. `materialSetRoughness`, from `0` for a mirror to `1` for chalk. `materialSetMetallic`, which is `0` for nearly everything -- paint, wood, plastic, skin -- and `1` for actual metal, with little use for the values between. And `materialSetEmissive`, which makes a surface glow with its own color for screens, lamps, and hot iron, without lighting anything around it. One thing to know about metal: a metal surface shows you its reflections rather than a color of its own, so in a scene with nothing to reflect it looks dark and dull. That is why the dragon's dark metal needs the sun to come alive. The proper fix is a sky (`sceneSetSky`), which wraps the scene in a panorama that both shows behind everything and lights it; the reference's "The Look of the Frame" covers it. === Movement the Model Brings with It Many models carry animations: a walk, a run, an idle, a door opening. They are stored in the file, and playing one is a single call on the instance's root node. [source,lua] ---- animationPlay(dragon, "Walk", true) ---- The second argument is the clip's name as the file stores it, or its number. `true` means loop it forever. Two more optional numbers set the speed and the crossfade: `animationPlay(hero, "Run", true, 1.2, 0.4)` plays the run a fifth faster and blends into it over four tenths of a second, so a walk becomes a run without a snap. `animationStop`, `animationPause`, and `animationIsPlaying` do what their names say. You cannot run that line yet, and here is the honest reason: the dragon in your `Singe` folder has no animations in it. It is a model, not a performance. Asking for a clip that is not there stops the game with an error, so never assume -- ask: [source,lua] ---- local clips = modelGetAnimations(dragonModel) if #clips > 0 then animationPlay(dragon, 1, true) else debugPrint("DragonModel.glb carries no animation clips. The wings are turned by hand.") end ---- `modelGetAnimations` hands back a table of the clip names in the file, in order, so `clips[1]` is the first one and `#clips` is how many there are -- the table calls from lesson six, on a table the engine filled in. Run it and watch the console: the message appears, because that count is zero. Get into the habit of that check with any model you did not make yourself. It costs three lines and it turns "the game died on startup" into a sentence in the log. === Moving the Parts Yourself A model with no animations is not a statue. Its parts are nodes, and nodes turn. [source,lua] ---- local wingLeft = nodeFind("wingL", dragon) local wingRight = nodeFind("wingR", dragon) ---- [source,lua] ---- function onOverlayUpdate() local flap = math.sin(singeGetTicks() / 260) * 22 nodeSetRotation(wingLeft, flap, 0, 0) nodeSetRotation(wingRight, -flap, 0, 0) nodeRotate(dragon, 0, 0.25, 0) overlayClear() overlayPrint(2, 2, "Singe/DragonModel.glb") overlayPrint(2, 4, "animation clips in the file: " .. #clips) return OVERLAY_UPDATED end ---- Save it. The wings beat, the dragon turns slowly on the spot, and the shadow on the floor beats with them. `singeGetTicks` is the number of milliseconds since the engine started, so it climbs steadily forever. `math.sin` takes that climbing number and gives back a value that slides smoothly from `-1` to `1` and back, a full beat about every second and a half at this divisor. Multiplying by `22` turns it into an angle of twenty-two degrees each way. Dividing by a bigger number slows the beat; multiplying by a bigger number widens it. That one line is the cheapest animation in existence and it is worth keeping in your pocket. The wings turn about their own X axis, and they turn around the shoulder rather than the middle of the animal, because whoever built the file put each part's node where the joint belongs. A model that was not built that way will swing its parts around the model's center instead, and the fix is to move the part's node, which the reference's Node section covers. Notice that the whole dragon turning does not interfere with the wings turning. Parts hang under the instance root, so rotating the root carries everything with it, and each wing's own rotation is on top of that. That arrangement -- a thing inside a thing inside a thing, each moving in its parent's world -- is what a scene tree is for. === Your Own Models When you make your own, export from Blender -- or Maya, or 3ds Max, or anything else modern -- as *glTF 2.0 binary*, which is a `.glb` file. That is Blender's default choice and it is the only model format Singe reads. The one rule is that the file must be self-contained: the shapes, the pictures on them, and the movements all inside the single `.glb`. A file that refers to a `.bin` or a `.png` sitting next to it is refused, with a message naming what it wanted. Blender's exporter does the right thing on its default settings. Not everything you can build survives the trip. The manual's "Models and Animation" section, in the 3D Scenes chapter, is the list: what is read, what is quietly ignored, and the limits (a skeleton may have up to a hundred and twenty-eight joints, for instance). Read it once before you spend a weekend on a model, not after. === Try It . *Kill the lights.* Comment out the whole `sun` block by putting `--` at the start of each line, and run it. Then do the same to `sceneSetAmbient`. Remember what that looks like, because one day it will happen when you did not mean it. . *Move the sun.* Change `nodeLookAt(sun, 0, 1.5, 0)` to `nodeLookAt(sun, 0, 20, 0)` so the sun aims over the dragon's head. Watch the shadow and the highlights go. Aiming a light is not decoration. . *Warm the torch up.* Raise the point light's intensity from `14` to `60` and drop its range to `4`. Then move it to `(0, 6, 0)`, above the dragon. A point light is a bulb, and you are choosing where the bulb hangs. . *Instance it twice.* Call `modelInstance(dragonModel)` a second time, keep it in another variable, scale it, and put it at `(-4, 0, -3)`. One load, two dragons. Then try loading the model twice instead and understand why you should not. . *Find another part.* The dragon's parts are named `head`, `neck`, `tail`, `body`, `crest`, `hornL`, `hornR`, `wingL`, `wingR`, `foreLegL`, `foreLegR`, `hindLegL`, and `hindLegR`. Make the tail sway with its own `math.sin` at a different speed from the wings. Then try `nodeFind("wings", dragon)` and see what a wrong name does when you hand the `nil` straight to `nodeSetRotation`. === Break It on Purpose Change the file name to `Singe/Dragonmodel.glb` -- one capital letter -- and run it. Singe stops before the window opens: ---- 15:modelLoad: Unable to read Singe/Dragonmodel.glb. ---- The line, the call, and the complaint, in that order. "Unable to read" means the engine looked and found nothing, which is nearly always one of three things: the name is misspelled, the capitalization is wrong, or the file is not where you said. On Windows the capitalization would have been forgiven and your game would then fail for everyone on Linux and macOS, which is a worse bug than this one. Type names exactly. The same message with a real file name means the file is not beside your script. The `Singe` folder is created in the folder you run the engine *from*, so if you started Singe from somewhere else, `Singe/DragonModel.glb` points somewhere else too. === What You Learned * `modelLoad` reads a `.glb` once; `modelInstance` places copies of it as often as you like. * An instance's root is an ordinary node: position, rotate, scale, and delete it like any other. * A model brings its own shapes and materials with it. * A model arrives at the size its author used, which is why one `nodeSetScale` on the instance is part of placing it. * A scene with no light is black or nearly so, and that is the first thing to check when a model looks wrong. * A directional light is a sun, a point light is a bulb that fades with distance and needs a much larger intensity, and a spot light is a cone. * Shadows are off until `lightSetShadow` turns them on, and each one costs. * `nodeFind` reaches a part of a model by its name from the file. * `modelGetAnimations` says what a model can play, and asking for a clip that is not there is an error. * A model with no animations can still be moved a part at a time. === Next Time Everything in this scene moves because you told it to, frame by frame. Next lesson hands that job to a physics engine: crates that fall, roll, stack, and knock each other over without you writing a line of arithmetic -- and the harder question of when you should let it. That is lesson twenty-four.