Kekouan

World Building

2023-06-13

The SceneKit starter jet model, inside of a 3D object that I created

From my notes at the time:

I managed to create two adjoining spaces connected by a cylinder. Some parts are inside out because of... reasons. I’ve tried to apply a “Concrete” material to the cylinder, but it mostly just looks grey.

I think I could’ve textured these things, but that’s a complication I don’t want to deal with at the moment.

The inside out nature is due to the normals being in the wrong direction. A constant bane!

Why not just use a Quake map, I say?

In my attempts to make my life easier I found a way to import Quake 1 maps into Blender.

E1M1 from Quake, in SceneKit

The Slipgate Complex is the first level of the Dimension of the Doomed. The level serves as a relatively gentle introduction to the gameplay of Quake (even on harder skill settings). It consists of two buildings with a small River) between them. The smaller building is where the player starts and it is relatively safer. The larger building is where most of the action takes place - it features several larger rooms, a large Slime) pool, and a series of platforms above it.

https://quake.fandom.com/wiki/E1M1:_The_Slipgate_Complex

E1M1 from Quake, in SceneKit

The textures are from a project to upscale the original Quake 1 textures. The lighting is also enabled, but it was neat to see.

E1M1 from Quake, in SceneKit, with the SceneKit starter jet model

Eventually I added in the jet model that you get from the SceneKit starter project. I was able to fly through the level, which was pretty exciting.

Best laid plans, undone

2023-06-06

The first thing I wanted to sort out was going to be some way of handling interactions between my "level geometry" and the player. There's a lot of ways to solve that set of problems, but I endeavoured to make SceneKit's problem so I didn't have to deal with it.

A SceneKit scene that has the default jet model, and two simple objects

A SceneKit scene that has the default jet model, and two simple objects

The base object in SceneKit Scene is called a SCNNode

A structural element of a scene graph, representing a position and transform in a 3D coordinate space, to which you can attach geometry, lights, cameras, or other displayable content.

So I've created two new nodes, given them the geometry of a pyramid and a sphere. They also have a physics system which means that when the jet model collides with the pyramid, it causes the wing to lift up. Or when the jet model collides with the sphere, it rotates.

Great. Excellent.

Levels, Mapping

One problem though, is that a game world populated entirely with spheres and pyramids is only so compelling. I need to be able to create arbitrary structures or layouts.

A screenshot of a Blender window, showing a two cubes joined by a corridor

The last time I spent any time with a 3D Modelling application was quite some time ago, and I preferred Modo. However in the intervening span of time, Blender has become the de-facto modelling application for people who do not have some reason to use things like Maya or the other high quality and very expensive applications.

It works though, surprisingly.

A SceneKit scene that has the default jet model, and two simple objects, and the model from Blender

The jet model dutifully bounces off the walls. I now have a way of creating things in Blender and making use of them in my project.

First things first

2023-05-29

One thing about using SceneKit is that you get to use a "Scene Editor", that lets you inspect the scene and add new nodes to it.

A screenshot of the SceneKit Scene Editor in XCode

The first thing I worked on was a way of getting the jet to move in various directions, and it was great to see that LightingShadows were just something you get for "free" depending on how you setup your scene:

A SceneKit scene with cubes displaying sides in light, and those in shadow

It's also possible to get realtime reflections:

A SceneKit scene showing the reflection of a cube on the shiny material of the jet model

The cube faces are having some issues due to not having great support for Metal on the GPU I was using at the time. If you look at the shiny material of the jet you can make out the reflection of the white cube below it.

This is why I abandoned my own attempts at writing a game engine, stand on the shoulders of giants, and all that.

Gotta start somewhere

2023-04-30

Upon hitting the rocky shores of reality with the dawning realization that I did not want to write everything from scratch, I searched for alternatives.

There are the ones that people might be aware of:

And some others that are similar but less well known. My main criteria was that it had to run on macOS, and preferably wasn't either C++ or C#. 1

That set of criteria left me with SceneKit.

Goal Setting

So at the end of June 2022, I decided to write-up my plan and start seeing where that would take me.

Kekouan

Grand Plans

I think I have accrued enough knowledge and hubris to attempt to create a vision of something and then attempt to execute that idea.

I’m going to try to attempt to create a game similar in substance to id Software’s 1992 Wolfenstein 3D. The original Wolfenstein 3D was broken up into episodes, the first one had 10 levels and I believe each episode had a “boss fight”.

It had 4 weapons:

• Knife

• Pistol

• Machine Gun

• Chain Gun

In the first episode I believe you only encounter 4 regular enemies plus the “boss”.

It also featured sound effects and music, which I will need to consider as well.

Things that I will consider to be “out of scope”:

Networking/Multiplayer

Verticality

Technical Aspects

I’m planning on creating this using Apple’s SceneKit framework. This is conceptually similar to the sorts of systems offered by Unity and Unreal Engine, in that it offers you things like:

SceneKit is a high-level 3D graphics framework that helps you create 3D animated scenes and effects in your apps. It incorporates a physics engine, a particle generator, and easy ways to script the actions of 3D objects so you can describe your scene in terms of its content — geometry, materials, lights, and cameras — then animate it by describing changes to those objects.

Plot / Theme

I don’t really want to re-hash the themes/plot to Wolfenstein 3D. I’ll fill this in later, perhaps.

Style / Substance

I’m not sure assets I have for this that I want to use, or what I’ll be able to procure or create. I’m going to focus on prototype things for now.

Goal

So what I’m trying to create is a single-player first-person shooter that has ~4 weapons, ~4 enemies, a boss, and ~10 or so levels. Ideally using assets I have either created myself, or licensed. I don’t really think there’s much in the way of demand for something like this as a commercial endeavour, but it could be something I toss in a portfolio / release for free.

And lo, this is what you get with the default SceneKit project:

The default SceneKit project, running

The jet model just rotates in space, you control the camera, and if you click on the jet it briefly turns red and blinks for a moment. Humble beginnings, but it has a physics system, sophisticated lighting/shadows and support for a surprising amount of 3D formats.


  1. Although there are now Swift bindings for Godot

Q2Model, Part 10

2023-04-29

Shadows

My plan to add shadows to Q2Model was to implement a depth map, but my shadows were only going to be computed from the view point of my Sunlight.

3D Model with depth map being applied

Just adding the depth map caused everything to get very dark.

View from the perspective

This is the perspective of the light that will be casting shadows, it's above and behind the model.

Partial shadows

The first evidence I had that shadows were working, but nothing really identifiable. It moved as the model moved though, so that was exciting.

I also had an interesting wall shadow:

Shadows on the wall

I never did figure out where that notch came from, but a couple of changes later I had working shadows:

3D model with shadows

And because I still had a collection of random Quake 2 models lying around: Quake 2 model of Chuckie from Rugrats

It was around this part that I started to see how daunting my plans/project were, if I was going to create all of this from scratch. I did spend some time re-working the lights and shadows to get to this point:

3D model with shadows and lights

But I was still only capable of creating shadows from the perspective of the "Sun".

3D model with shadows and lights

However in doing so I had to remove the text I had that displayed the animation/skin.

That's as far as I got with this. It was at this point I decided that writing a 3D game engine, and also the renderer was going to be too big to tackle. It did however accomplish the goal of making me learn how things worked from the ground up.

Q2Model, Part 9

2023-04-28

Lights, Shadows

During all of this, I did occasionally think about Forsaken) which came out a year after Quake 2. It was also a "Six Degree of Freedom" game like Descent, but they went all in on the lighting system.

A screenshot of Forsaken, from the Forsaken Remastered project

It's atmospheric, it's slightly green tinged, wonderful.

I think you'll agree that you need to have lights for things to be

So I started working on adding lights into my project.

What I had at this point, after swapping out the skybox textures for something more Quake 2 like:

Q2Model, rendering the Dragon Ogre model, at full brightness

There is no specific or directional lighting being applied, the model is as bright as the texture is in all locations.

Q2Model, rendering the Dragon Ogre model, with lighting effects

Now I've placed a light above the model, slightly behind. You can see that parts of the model that would be obscured are now much darker. The model itself doesn't cast a shadow, so the room/walls remains unchanged.

Same model, but with some lights that are not just white

Added some delightful coloured lights. Not entirely sure what happened with that back wall texture though.

3D Model with coloured lights

Here's where I start attempting to make the room/walls also receive lighting. Not quite there yet, but it didn't take long:

Room with proper lighting, including more coloured lights

Decided to see what it would look with a different model:

3D Model of a puffin with a fish

At this point I had added support for 4 kinds of lights

It's probably easiest to start with the last two. A point light is a light source that radiates in all directions. See also this blog post on Pluralsight, where I have taken these images from.

Point light source, radiating in all directions

Candles, Incadescent Bulbs, are what this would invoke. Generally there is no spot where the light does not radiate, unlike their physical counterparts. You typically get to set a colour (or temperature), the intensity of the light, and how quickly it attenunates or "drops off" as the distance increases.

Spot lights are, unsurprisingly, what they say on the tin. Spot light source, a conical light pattern

Spot lights have a direction of the cone, the angle of the cone, and how much it falls off from the center of the cone in addition to the distance.

Sunlight or directional lighting is the sort of light you'd get from the Sun, it's directional, but goes on forever (+/- attentuation values).

Directional light

Ambient Light is just a sort of "global illumination":

3D Model displaying ambient light, even the the parts that are in "shadow" are illuminated

This is useful to add lighting so everything isn't completely dark if not lit.

Q2Model, Part 8

2023-04-01

To the Walls!

After I had figured out a solution for getting some way of displaying things, I combined them together to great effect:

Dragon Ogre model, not displaying correctly

"RUN000" being the specific frame it is displaying, and "DRAGON_OGRE" is the name of the skin.

Dragon Ogre model rendering properly

I managed to get the model displaying correctly but I broke the display of the text. It's cutting things in half, it's still attempting to display "RUN000" but the last part got cut off.

Okay so it turns out when I turned my text from 2D into 3D I didn’t update everything I needed to update. I was allocating space for N things that had 2 positions, then I came along and filled in N things with 3 positions.

So the animation text worked because it was actually something like RUN00, so 5×2 = 10, and 3×3=9 so it had enough space to work.

Easy fix, in retrospect but it took me some time to track it down.

From my notes at the time.

Dragon Green model rendering, with proper text support

I was still browsing through models at the time:

Marvin the Martian model

Timeless.

Walls, and how to hit them

My original plan for this was to use this to create game similar to Wolfenstein 3D, and that definitely had walls. So that was my next ambitious goal, with a bit of misadventuring.

A model being rendered, and something like walls and a floor are visible

My first implementation was to attempt to just generate some flat planes and hook them together. One corner of a cube, and everything attached, essentially. This was not a plan that was going to work.

Skybox

I decided to do what everyone else does and just make a large cube and stick everything inside of that.

Outdoor scene in a Quake 2 level, showing the Skybox

In Quake 2 for example, you've got the level geometry and then the reddish-orange sky that depicts what it would look on Stroggos. That is effectively the inside of a large cube.

A skybox image, unwrapped

That's one representation of a skybox texture, with the sides labelled. Saves it from looking like things are taking place inside of a black void.

I also started with a Skybox texture, as there are numerous versions floating in the aether.

Dragon model rendered inside of a cube with a skybox texture

cube with skybox texture, with edges that do not match

Depending on how your directions are defined, you find that you have to rotate certain images to fit properly.

Dragon model rendered inside of a cube with a skybox texture

Q2Model, Part 7

2023-04-01

The fun and exciting thing that figuring out the PCX file format allowed me to, was to wander through a large collection of models people had created.

an animation of a Spongebob Squarepants model an animation of a Bart Simpson model an animation of a Neon Genesis Evangelionmodel

Check out that neat shield effect!

an animation of a Johnny 5 model an animation of a dragon model

Delightful.

I ended up switching to the Dragon model as it was very well done and quite a lot of work put into it.

Adventures, in other directions

multiple astroboy models

Among other things I also figured out how to have Metal render the model several times. One would use this sort of thing if you had multiple copies of things in a given "scene", think trees and foliage.

Bitmap Fonts

At this point I had implemented the ability to switch between animations and skins, but it wasn't entirely clear what I was looking at. I needed some way to display information.

Quake bitmap font

That's a bitmap font called "Grey Bugs" by Michal Sochon (KaszpiR). My first attempt didn't work out so great:

bitmap text attempt one

That is supposed to be "Hello", but due to how I am sampling the texture (the bitmap font) it's not working properly. In short order I managed to get this, which is a definite improvement:

bitmap text attempt two

Small problem in that I was specifying the coordinates in the incorrect order so it was flipped and mirrored.

Still, I was quite pleased that I was able to get this to work. There were solutions that involved using the macOS text rendering system to render the text into an image and then displaying that, but that seemed overkill. This is just constructing a series of triangles together (as you can see in the first image), and applying a texture to them.

Q2Model, Part 6

2023-03-13

Actually, I went back to look at what I worked on after getting the model to render with a texture. Turns out it was animations and figuring out the PCX file. Indexed primitives would turn out to be a colossal pain, but I figured out animations and the PCX file first.

Animations

a frame of the Astroboy animation

a different frame of the Astroboy animation

The way animations work in the Quake 2 Model format is to store the vertex positions of every vertex at that point in the animation. So the stand animation for this model is 40 frames. My rough initial version looked like this:

an animation of the Astroboy stand animation

It's going a bit quickly and I am not interpolating the values between frames, so it's fast and jerky.

There appears to be a standard set of animations, but I've seen models where people have added their own animations:

Some of these are only ~10 frames, whereas the the stand animation has 40 frames in the Astroboy model.

Through the cunning use of interpolation (instead of switching to the next position immediately, transition between the two) and slowing it down a bit:

an animation of the Astroboy stand animation, with interpolation

Delightful.

PCX Files

The textures for Quake 2 Models are stored as PCX files, a delightful image format that was getting long in the tooth even in 1997. I thought this was going to be a vastly more difficult problem to solve. I had the good fortune to find someone who wrote a Quake 2 Software renderer in Swift, and they had to contend with loading PCX files as well.

A bit of tinkering later, and I had this to show for it:

Astroboy model with a texture loaded from a PCX file

Everything is brighter now for unknown reasons, is this the way it's supposed to look? Unknown.

Q2Model, Part 5

2023-02-26

Initially, before writing some Swift code to read in a PCX file, I took the option of just converting the texture to a modern file format (PNG).

Astroboy Texture in PNG format

A textured Astroboy being rendered

That's the result of swapping out the texture that formerly covered a rotating cube with the Astroboy texture. It was surprisingly easy to get this up and going, which I guess is partially the point of having a graphics API.

This is still somewhat un-optimized as I am just drawing "primitives":

renderEncoder.setFragmentTexture(texture, index: TextureIndex.color.rawValue)

for (i, vertexBuffer) in q2mesh.vertexBuffers.enumerated() {
    renderEncoder.setVertexBuffer(vertexBuffer,
                                  offset: 0,
                                  index: i)
}

renderEncoder.drawPrimitives(type: q2mesh.primitiveType,
                             vertexStart: 0,
                             vertexCount: q2mesh.vertexCount)

drawPrimitives(type:vertexStart:vertexCount:)

Encodes a command to render one instance of primitives using vertex data in contiguous array elements.

Drawing starts with the first vertex at the array element with index vertexStart and ends at the array element with index vertexStart + vertexCount - 1.

When a draw command is encoded, any necessary references to rendering state or resources previously set on the encoder are recorded as part of the command. After encoding a command, you can safely change the encoding state to set up parameters needed to encode other commands.

https://developer.apple.com/documentation/metal/mtlrendercommandencoder/1516326-drawprimitives

At this point I wanted to do two things, read in the PCX file directly (this would let this work on any Q2 Model) and switch to "indexed primitives."

Indexed Primitives

The Astroboy model is made up of 341 vertices, that are part of 552 triangles. Imagine, if you will, a polygon:

A pentagon inscribed inside of a circle

There are 5 triangles, but they all connect to the center of the circle, and each triangle shares a point with its nearest neighbour. So there's a total of 6 unique points, but if you treat each triangle as it's own entity you'd have to specify 15 points in total.

A decagon inscribed inside of a circle

Same idea here, 11 unique points, but if you treat each vertex or triangle separately that's 30 points. Quake 2 Models can only have a maximum of 2048 vertices, but that's very low compared to a model you'd be dealing with today. So it's a good idea to optimize this by providing a list of all the vertices, and then just referencing those vertices by index number when constructing triangles.