431 lines
17 KiB
Text
431 lines
17 KiB
Text
== 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 manual'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 manual'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 manual'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.
|