== Lesson 22: Into 3D image::learn/22-3d.png[The finished lesson, 480] Everything you have drawn so far has been flat. Sprites, text, boxes, and lines all live on the overlay, which is a sheet of glass at the front of the window with the same coordinates it had in lesson one. This lesson puts a world behind that glass: a place with depth, where a thing can be nearer or further away, and where a camera decides what you see of it. By the end you will have a box floating in space that you can turn with the arrow keys, and the numbers for its angles printed in the corner by the same `overlayPrint` you used on your very first day. That last part matters more than it sounds, and there is a section about it. === A Scene, a Box, and a Light Make a folder, put a file called `spin.singe` in it, and type this in. [source,lua] ---- sceneEnable(true) sceneSetBackground(18, 20, 34) sceneSetAmbient(35, 35, 45) local paint = materialNew() materialSetColor(paint, 215, 95, 60) materialSetRoughness(paint, 0.6) local box = nodeNew() nodeSetMesh(box, meshBox(2, 2, 2), paint) local sun = lightNew(LIGHT_DIRECTIONAL) nodeSetPosition(sun, 4, 6, 5) nodeLookAt(sun, 0, 0, 0) function onOverlayUpdate() nodeRotate(box, 0, 0.5, 0) return OVERLAY_UPDATED end ---- Run it with `Singe -R spin`. An orange box turns slowly on a dark blue background, lit from above and to the right, with one face bright and the others falling away into shadow. There is no camera in that script. Singe put you at `(0, 0, 5)` looking at the middle of the world, because a scene with no camera has to be seen from somewhere. You will replace that with your own camera in a moment. If the window stays black, read the console. On a machine whose graphics cannot draw a 3D scene, `sceneEnable` stops the game with a message saying so, and there is nothing to do about it but run the lesson elsewhere. 2D games keep working on such a machine; 3D does not. === What Just Happened The three calls at the top set up the world as a whole. [source,lua] ---- sceneEnable(true) sceneSetBackground(18, 20, 34) sceneSetAmbient(35, 35, 45) ---- `sceneEnable(true)` turns the 3D layer on. It is the one call that every 3D game makes, and until you make it nothing you build in the scene is drawn. `sceneSetBackground` is the color the scene is wiped to each frame, red, green, and blue from `0` to `255`, the same way colors have worked since lesson nine. There is a fourth number you can give it for transparency, which is how a game that plays video puts a 3D object in front of the film. Leave it off and the background is solid. `sceneSetAmbient` is a little light coming from every direction at once. Its job is to keep the sides that no lamp reaches from being pure black. Turn it down to `0, 0, 0` later and see how hard the picture becomes. [source,lua] ---- local paint = materialNew() materialSetColor(paint, 215, 95, 60) materialSetRoughness(paint, 0.6) ---- A *material* is how a surface looks: its color, how shiny it is, whether it has a picture on it. `materialNew` makes one and hands back a number that stands for it, the same kind of handle `spriteLoad` gave you in lesson nine. Keep it in a variable, because every later call needs it. `materialSetRoughness` runs from `0`, a mirror with a tiny hard highlight, to `1`, a chalky matte surface with no highlight at all. `0.6` is paint on wood. There are a dozen more `materialSet` calls, and the next lesson uses some of them; the manual's Material section lists them all. [source,lua] ---- local box = nodeNew() nodeSetMesh(box, meshBox(2, 2, 2), paint) ---- This is the important one. A *node* is a place in the world. It has a position, a rotation, and a scale, and on its own it draws nothing at all: it is a spot that things can be attached to. Everything in a 3D scene is a node, including the lights and the camera. `nodeNew` makes an empty one at the middle of the world. `nodeSetMesh` hangs two things on it: a *mesh*, which is the shape, and the material, which is how that shape looks. `meshBox(2, 2, 2)` builds a box two units wide, two high, and two deep, and hands back a handle to it the way `materialNew` did. Meshes and materials are worth sharing. One `meshBox` handle can be used by a hundred nodes, and that is how you draw a hundred crates cheaply. Making a hundred separate boxes that happen to be the same size is the beginner's way to make a scene slow. [source,lua] ---- local sun = lightNew(LIGHT_DIRECTIONAL) nodeSetPosition(sun, 4, 6, 5) nodeLookAt(sun, 0, 0, 0) ---- `lightNew` also makes a node -- one carrying a light -- and hands back its node handle, so you place and aim it with the same calls you use for everything else. `LIGHT_DIRECTIONAL` is a sun: it is so far away that only the direction it shines in counts, which is why the position here matters only in that `nodeLookAt` uses it to work out the direction. Take those three lines out and run it again. The box goes nearly black, because the only light left is the dim ambient. Light is the whole of lesson twenty-three, and this is the first taste of the most common disappointment in 3D: the model loaded fine, and the scene is black because nothing is shining on it. [source,lua] ---- nodeRotate(box, 0, 0.5, 0) ---- `nodeRotate` turns a node by so many degrees about each of its own three axes, on top of whatever rotation it already had. Half a degree a frame, at about sixty frames a second, is a turn every twelve seconds. This is the same trick as lesson two: a small change, applied every frame, reads as movement. === Which Way Is Which Three numbers describe a place in the scene, and you have to know what each one means before you can put anything anywhere. * *X* runs to the right. Larger X is further right. * *Y* runs up. Larger Y is higher. The floor of a scene is usually `y = 0`. * *Z* runs toward you, out of the screen. Larger Z is nearer the default camera, and *negative Z is the direction the camera looks*. That last one catches everybody. Things you want in front of the camera go at negative Z. It is not a quirk of Singe: glTF and Blender use the same arrangement, so a model exported from Blender arrives facing the way Singe expects. Do not take my word for any of it. Add this line after the `nodeSetMesh` line, save, and look: [source,lua] ---- nodeSetPosition(box, 3, 1, 0) ---- The box jumps to the right and up. `nodeSetPosition` puts a node at an exact place, and it replaces whatever position the node had. There is a matching `nodeMove(node, dx, dy, dz)` that shifts a node by an amount instead, along its own axes, so `nodeMove(ship, 0, 0, -0.1)` drives a ship forward whichever way it happens to be pointing. Try `nodeSetPosition(box, 0, 0, -20)` next. The box shrinks into the distance, which is negative Z doing its job. Then try `(0, 0, 20)`: the box vanishes, because you have put it behind the camera. Two more calls finish the set. [source,lua] ---- nodeSetRotation(box, 0, 45, 0) nodeSetScale(box, 0.5) ---- `nodeSetRotation` sets the rotation outright in degrees, where `nodeRotate` adds to it. The three numbers are about X, about Y, and about Z: turning about Y is what a person standing on the floor does when they turn to face a different way, and it is the one you will use most. `nodeSetScale` makes the node bigger or smaller, either with one number for all three axes or with three for one each. Take both of those experiments back out before you go on. The rest of the lesson starts from the script as you first typed it. === Your Own Camera The default view is a courtesy, not a feature. Put a camera in. [source,lua] ---- local camera = nodeNew() nodeSetPosition(camera, 0, 2, 7) nodeLookAt(camera, 0, 0, 0) cameraSet(camera) cameraSetPerspective(60, 0.1, 100) ---- Put that under the light, before your `onOverlayUpdate`, and run it. You are now looking slightly down at the box from two units up and seven back. A camera is a node like any other. `cameraSet` tells the engine which node to look out of, and the scene is drawn from that node's position looking down the node's own negative Z. Aiming a camera by hand with `nodeSetRotation` would be miserable, so `nodeLookAt` does it for you: give it a point in the world and it turns the node to face it. `cameraSetPerspective` is the lens. The first number is the field of view in degrees, up and down: how much of the world is squeezed into the height of the window. A small number like `35` is a telephoto lens that flattens everything; a large one like `90` is a wide angle that makes the room look enormous and the corners bulge. `60` is a comfortable default. The other two numbers are the nearest and furthest distances that get drawn, and things outside that range are not drawn at all. Because the camera is an ordinary node, everything you learned about nodes works on it. Later you will hang a camera underneath a moving car and get a chase camera for nothing. === Turning It with the Arrow Keys Reading the coordinate system off a page is a poor way to learn it. Moving something around in it is a good one. Replace your `onOverlayUpdate` with this, and add the two new callbacks and the four variables above it. [source,lua] ---- local turnLeft = false local turnRight = false local tiltUp = false local tiltDown = false ---- Those go at the very top of the file, above everything else. [source,lua] ---- function onInputPressed(what) if what == SWITCH_LEFT then turnLeft = true elseif what == SWITCH_RIGHT then turnRight = true elseif what == SWITCH_UP then tiltUp = true elseif what == SWITCH_DOWN then tiltDown = true end end function onInputReleased(what) if what == SWITCH_LEFT then turnLeft = false elseif what == SWITCH_RIGHT then turnRight = false elseif what == SWITCH_UP then tiltUp = false elseif what == SWITCH_DOWN then tiltDown = false end end function onOverlayUpdate() if turnLeft then nodeRotate(box, 0, -1.5, 0) end if turnRight then nodeRotate(box, 0, 1.5, 0) end if tiltUp then nodeRotate(box, -1.5, 0, 0) end if tiltDown then nodeRotate(box, 1.5, 0, 0) end return OVERLAY_UPDATED end ---- `onInputPressed` and `onInputReleased` are callbacks, like `onOverlayUpdate`: you write them, and Singe calls them when a control goes down or comes back up. A press arrives once, not over and over while the key is held, which is why each one sets a variable and the drawing callback does the work. That pattern -- press sets a flag, the frame reads the flag -- is how you get "while the key is held" out of an engine that only tells you about changes. The `SWITCH_` names come from the engine itself, so nothing has to be loaded to use them. They are not keys, either. They are what a control *means*, and `controls.cfg` decides which key, button, or stick direction produces each one. Your arrow keys send `SWITCH_LEFT` and friends today; a gamepad's stick sends the same thing, and your script never knows the difference. The signs are chosen so that each arrow moves the face you are looking at in the direction you pressed. Turning about Y by a positive angle swings that near face to the right, so the left arrow asks for a negative one. If you find that backwards, swap the signs; they are your controls, not the engine's. === The Overlay Is Still on Top Add these lines to `onOverlayUpdate`, above the `return`. [source,lua] ---- local pitch, yaw = nodeGetRotation(box) overlayClear() overlayPrint(2, 2, "Arrow keys turn the box.") overlayPrint(2, 4, "yaw " .. math.floor(yaw) .. " pitch " .. math.floor(pitch)) ---- Nothing there is new except what it is printed over. `overlayPrint` is the call from lesson one, counting in character cells, needing no font loaded, and it is drawing on top of a 3D scene without being told anything about it. That is the arrangement, and it is worth saying plainly: Singe draws the video first, then the 3D scene over it, then the overlay over that. Your score, your lives, your timer, your crosshair, and your subtitles are all overlay work, exactly as they were in part two. A 3D game keeps its score in the corner with the same five lines a 2D game uses. `nodeGetRotation` hands back three numbers, the angles about X, Y, and Z, and this line keeps the first two in `pitch` and `yaw`. Those are the usual names for them: pitch is the nose going up and down, yaw is turning left and right. `math.floor` throws away the fraction so the numbers stop flickering. Watch the yaw as you hold the left arrow. It counts down past `-180` and comes back round from `180`, because the angles are reported as the smallest turn that gets you there rather than as a running total. If your game needs to know how many times the player has spun, count that yourself. === Try It . *Move the camera.* Change `nodeSetPosition(camera, 0, 2, 7)` to `(0, 8, 7)`, then `(7, 2, 7)`, then `(0, 0.2, 7)`. Each time the `nodeLookAt` keeps the box centered, so you get a different angle on the same scene for one number. . *Change the lens.* Try `cameraSetPerspective(30, 0.1, 100)`, then `(90, 0.1, 100)`, without moving the camera. A game feels fast with a wide field of view and stately with a narrow one, and this is the cheapest mood knob there is. . *Put a second box in.* Copy the three lines that made the box, rename the variable to `mark`, and give it `nodeSetPosition(mark, 3, 0, 0)` and `nodeSetScale(mark, 0.3)`. Now you have a landmark to judge the first box against. Move it to `(0, 3, 0)` and `(0, 0, -3)` in turn and say out loud which axis is which. . *Share the mesh.* Instead of calling `meshBox` twice, keep the first one in a variable and hand the same handle to both nodes. Nothing looks different. That is the point: one shape, two places. . *Hang the camera on the box.* Keep the second box from the last step, or there will be nothing left to move against. Then look up `nodeSetParent` in the manual and hang the camera under the first box. Hold an arrow key: the camera rides the box's rotation, so the box sits still and the rest of the scene swings past. That is one line away from a first-person view, and it is also how you find out that a chase camera wants a parent rather than arithmetic. === Break It on Purpose Mistype the material's name. Change the `nodeSetMesh` line to use `pain` instead of `paint`: [source,lua] ---- nodeSetMesh(box, meshBox(2, 2, 2), pain) ---- Singe stops before the window opens and prints something like: ---- 17:nodeSetMesh: Argument 3 must be a number. ---- This is a different shape of error from the one in lesson one, and it comes from the engine rather than from Lua. The number at the front is the line. The name after it is the call that complained. The complaint itself is that the third thing you handed it was not a number. The reason is worth following, because it explains a whole family of errors. `pain` is a variable that was never given a value, and in Lua an empty variable is `nil` -- the value that means "nothing here". Handing `nil` to a call that wants a material handle gives exactly this. When you see "must be a number" from an engine call, look for a misspelled variable name on that line, or for a handle you forgot to keep. === What You Learned * `sceneEnable(true)` turns the 3D layer on, and nothing is drawn in 3D until you call it. * A node is a place in the world with a position, a rotation, and a scale. Meshes, lights, and cameras all hang on nodes. * A mesh is the shape and a material is the look, and one of each can be shared by any number of nodes. * X is right, Y is up, and negative Z is the way the camera looks. * `nodeSetPosition` and `nodeSetRotation` set a value outright; `nodeMove` and `nodeRotate` change it by an amount. * Any node can be the camera. `nodeLookAt` aims it and `cameraSet` chooses it. * The field of view in `cameraSetPerspective` changes how the scene feels without moving anything. * A scene with no light is black, which is the first thing to check when nothing appears. * The overlay is still there, still on top, and still where your score goes. === Next Time A box is a poor dragon. Next lesson loads a real model -- one that the engine already unpacked into your work folder, so there is nothing to download and nothing to draw -- places it on the floor, and lights it properly. That is lesson twenty-three.