Renderer

OpenReliant draws with Surrender's own pipeline, ported function by function into the modules of its original files, and with its Direct3D 7 driver, ported to draw through a device interface in place of IDirect3DDevice7.

Module Original Does
surrenderlib/srcore.zig srCore.cpp The scene's lists; a frame, layer by layer; the sort of what the driver puts aside
surrenderlib/srmesh.zig srMesh.cpp The mesh pipeline: view test, level of detail, culling, projection or clipping, lighting
surrenderlib/srbmo.zig srBMO.cpp The sprite pipeline
surrenderlib/srstars.zig srstars.cpp The star pipeline
surrenderlib/srapi.zig srAPI.cpp The projection; a mesh's planes and bounds
surrenderlib/srapiext.zig srAPIext.cpp Meshes, mesh objects, sprite sets
srd3d/srd3d.zig srd3d.dll The driver: render states, batching, clipping, the sun test
srd3d/device.zig Direct3D 7 The device the driver draws with
srd3d/software.zig A device that rasterizes as Direct3D 7 does, in software
platform/gpu.zig Direct3D 7 The device the openreliant executable draws with: SDL's GPU interface
platform/shaders/device.glsl Direct3D 7's texture stages The GPU device's shader
surrenderlib/srshadow.zig, platform/gpu/shadows.zig, platform/shaders/shadow.glsl OpenReliant's shadows: the maps' boxes and casters, and the GPU's depth passes
platform/gpu/geometry.zig The GPU device's vertex and index buffers
game/srofiles.zig srofiles.cpp Meshes from .SHP models
game/objects.zig objects.cpp A live object's part nodes, placed and drawn
game/nebula.zig, game/backdrop.zig nebula.cpp, backdrop The sky dome, the nebula, the stars, the dust, the sun, the lights
game/xtrabits.zig xtrabits.cpp scene_add

A scene object's kind picks its pipeline: 1 a mesh, 4 a sprite set and 7 a star field. Kinds 5 and 6 are dead, and are described below.

The software device is the reference the GPU device is checked against: the same scene gives the same image. Pixel centres lie at whole numbers, as in Direct3D 7; screen positions are kept in sixteenths of a pixel, and a pixel whose centre lies on an edge belongs to the triangle whose top or left edge it is. Colours, alpha and texture coordinates are interpolated in perspective, depth straight across the screen. Textures are sampled bilinearly, wrapping, from the mip level nearest to the texels a pixel spans.

The openreliant executable draws with the GPU device, or with the software device when asked (Platform). sltool render draws a model against the backdrop through all of it onto the software device, two frames as the game draws them one after another, the first finding how much of the sun shows; make render draws the Predator toward the nebula and toward the sun into game/renders/.

The GPU device

The GPU device draws what the driver hands over with SDL's GPU interface: Metal on macOS, Vulkan elsewhere. The driver draws a strip, a fan or a single blended polygon at a time, and the game's textures are small, so the device gathers a frame before drawing it:

  • Each texture is a layer of a texture array holding the textures of its width, height and number of levels, up to 256 layers, the fewest Vulkan guarantees; a texture goes up to the GPU, every level, the first time it is drawn, and the device keeps its array and layer in the image's device, as the driver's texture_upload keeps the device texture it makes.
  • Each vertex names its texture's layer, or none. Strips and fans become lists of triangles, and consecutive draws with the same primitive and render states, whose textures share an array, go to the GPU as one draw.
  • A pipeline is made for each primitive and set of render states the driver draws with, every layer's and blend mode's, as the device starts: the depth test greater or equal, depth being reversed, and the driver's blend factors. The system compiles a pipeline's shaders for its states, which can take a quarter of a second the first time, so that one made as an effect first shows would hold up its frame. Depth is clamped rather than clipped, as Direct3D 7 did not clip transformed vertices in depth, and the pipelines write colour only, as the original's back buffer kept no alpha.
  • Textures wrap, as Direct3D 7's did by default, which the meshes' texture coordinates rely on. What is drawn over the finished frame, the display's and the menus' images, is read held at its edges instead: the original drew those with VFX, unfiltered, and a filter at the edge of an image that wraps reads the texels of its far side, which showed as thin lines along the edges.

The brightness (Video) is the display's gamma ramp, which the original's driver set (set_gamma, 0x10005270): the driver hands it to the device as each frame begins. Where it is other than 1, the GPU device puts the finished frame on the window through it, in a last pass of bloom.glsl, each channel at its power 1 over the brightness, so that the ramp takes in the display and the menus as the original's did; a screenshot keeps the frame as drawn, as the original's did. The software device has no ramp, and the brightness is hidden, as on hardware without one.

The device's shader, device.glsl, takes the driver's vertices as they are: screen positions with pixel centres at whole numbers, reversed depth, and rhw, whose inverse as the clip-space w makes colours and texture coordinates vary in perspective. The fragment is the texel times the vertex colour, or the vertex colour alone. For a lit mesh, the shader first adds the frame's directional and point lights to the vertex colour for the pixel, the key lights' share scaled by the shadows (Improvements). make shaders compiles it and the shadows' depth pass, shadow.glsl, with glslc into SPIR-V, and from that into Metal's language with SPIRV-Cross, which make builds; the outputs are committed, so building the game needs neither. Beside them it writes the SHA-256 of each shader's source, and a test fails when a shader's source no longer matches it, so that a changed shader isn't left uncompiled (programs.zig).

Compressed textures

Improvement: mods' pictures go to the GPU compressed where it takes BC formats (Compression, mod_pictures.zig).

  • A picture can come compressed already, in a DDS or KTX2 file, which draws as it is. A KTX2 file's levels supercompressed with Zstandard are inflated with std.compress.zstd as the file is read (ktx2.inflated), into a plain KTX2 file of the same picture.

  • A level says how it holds its pixels (srtexture.Level.Format, formats/texels.zig): 8-bit RGBA, 16-bit RGBA or RG, or BC1, BC3, BC5 or BC7 blocks. A 16-bit PNG normal map is read and mipmapped in 16-bit RGBA (png.readWide, srtexture.mipmapsWide), then kept as 16-bit RG on the GPU, or compressed to BC5 by OpenReliant's own encoder from its 16-bit samples (texels.bc5), which picks each block's endpoints and indices for the least squared error. A texture array holds textures of one format as well as one size and number of levels (Shape). Its images are read decoded from sRGB in linear light, as 8-bit ones are; its normal maps are BC5 and its material maps and emissive maps BC7, beside a compressed picture.

  • The GPU says which compressed formats it takes for arrays, plain and sRGB alike (Gpu.compressed, Gpu.takes). The texture table compresses a mod's picture only where it takes BC5 and BC7 (srtexture.Compressor), with bc7enc and rgbcx from bc7enc_rdo, built from a pinned source package (deps/texture-compressor), the rows of blocks shared out between threads (platform/texture_compressor.zig).
  • Improvement: a model's mods' pictures load together (srtexture.Table.prefetch, called by srofiles.modelLoad). Their files are read on the game's thread, decoded on as many threads as the computer has cores, each picture's maps on threads of their own, and made ready for the device on the game's thread again. PNG files inflate with zlib, built from a pinned package, which is faster than std.compress.flate, and each mipmap level's rows are shared out between threads (srtexture.shareRows). The original finds each texture as it builds the mesh that shows it.
  • What it compressed is kept in the game folder's cache/textures (platform/texture_cache.zig): a file for each texture, named by a hash of its name, with a 32-byte header (ORTX, the layout's version, the table's key of the picture's files and the texture detail, an XxHash3 of the rest and its size), then the picture's levels and its maps'.
  • A BC5 normal map holds x and y alone. The shader works out z, and reads the length of the normals' mean from the material map's alpha, where the table moved it before compressing (Shading.two_channel_normals).
  • Once the GPU has copied an image's pixels for upload, it marks the image held (Image.held), and the texture table lets go of its own copy after the frame (Table.releaseHeld). It keeps them with the software device, which reads them. Code that reads an image's pixels checks Image.readable first, as the sun's redrawing does.

Shadows

Improvement: the key lights cast shadows, where the original drew none. srshadow.zig gathers them each frame and the GPU device draws them (gpu/shadows.zig):

  • The maps. The view is split by depth into four cascades, each an orthographic box along the sun around the sphere that holds its slice of the view. Its centre is moved to a whole texel of the world and its axes follow the world, so the shadows hold still as the camera moves and turns. The cockpit, where the scene holds one, gets a fifth box around its parts. The maps are the layers of one depth texture.
  • The casters. Every lit mesh of the world's layer casts, whatever the camera sees of it: its opaque surfaces at its current level of detail, each polygon a fan of its corners, turned into the camera's frame each frame. A mesh whose light mask keeps out every key light casts none, as the sun lights it not: the Reliant's hangar keeps the sun off its hull and doors so, and the ship launching within it shows lit, as in the original (Launches). The ship the camera sits in casts without being drawn (srcore.Scene.casters), into the cascades alone, as the cockpit sits inside it. The cockpit's parts cast into its map alone, and a ship between the cockpit and the sun casts into it too. Each caster goes into the maps its bounding sphere can reach. A mesh cut by a portal casts only what the portal keeps, as a splitting capital ship is drawn. Blended surfaces and sprites cast nothing, but for a cloaking part's see-through hull (srapiext.MeshObject.alpha_shadow), which casts as strongly as the hull is solid, so a ship's shadow fades out with it as it cloaks (The cloak).
  • The depth pass. Before the frame, each map is cleared and its casters drawn, depth alone and both faces, with a slope-scaled bias. What lies nearer the sun than a box is held at its near side rather than cut off, so that it still casts. A caster that casts less than a whole shadow is drawn into that share of the texels, in an ordered pattern over each four by four, which the lookup's taps blend into a lighter shadow.
  • The lookup. A world pixel takes its shadow from the first cascade that reaches as deep as it stands, the cockpit's from its own map. Its place is moved a texel and a half along its normal, so that a surface does not shade itself, and a square of taps, each comparing the four texels around it, softens the edge. Only the key lights (srlight.Light.shadowed) are scaled, and only where they face the pixel; the fill and ambient lights are not, so a shadowed hull keeps the nebula's colour. The last cascade fades out toward its end. Point lights cast no shadows.
  • The cockpit's are a little lighter and softer: a full shadow takes away 70% of the sun, and the taps spread three times as wide. The cockpit takes both key lights, and its map's texels are fine enough to make the edges razor sharp otherwise.
--shadows Maps Taps Cascades end at
low 1024 texels across 4, a texel apart 1,500, 6,000, 20,000 and 60,000
high, the default 4096 texels across 16, 1.4 texels apart 2,500, 10,000, 35,000 and 120,000

--shadows off, --original and --no-pixel-lighting leave them out, and --no-cockpit-shadows the cockpit's alone. The software device draws none. Not yet: fitting the cascades to the objects in them, which space leaves mostly empty (#196).

Runtime shader compilation

Improvement: shader_compiler.zig compiles GLSL as OpenReliant runs, into SPIR-V for Vulkan and Metal's source for Metal: mods' post effects, the variants of the device shader with mods' functions in them (Surface and lighting functions), and mods' replacements for OpenReliant's shaders (Replacements). It links glslang 16.1.0 and SPIRV-Cross vulkan-sdk-1.4.357.0, built from pinned source packages. OpenReliant's own shaders are compiled ahead of time by make shaders, and their outputs are committed.

  • A shader is GLSL 450 for Vulkan 1.0 and SPIR-V 1.0, translated to Metal 2.2. It can be made of several parts, each named in the messages (compileParts), after a preamble of definitions such as #define FRAGMENT. A part cut from the middle of a file keeps the file's line numbers.
  • Includes are rejected: OpenReliant puts colour.glsl in place of the line that includes it (gpu/programs.zig). So are NUL bytes, and a part over 1 MiB.
  • A C++ wrapper catches the libraries' exceptions. Zig owns copies of the code or of the messages. Calls take turns, as glslang starts and stops once for each.

A post effect is a fragment shader with one vec2 input and one vec4 output, both at location 0. It may read gl_FragCoord, but write no built-in output. It may declare up to two float sampler2D textures at set 2, bindings 0 and 1, and a std140 uniform block at set 3, binding 0, of exactly two vec4 fields. These are SDL's resource sets for fragment shaders, and Metal's binding numbers follow from them. Arrays, storage resources, push constants and specialization constants are rejected. Reflection checks all this before the Metal translation, and a failure names the file and what doesn't fit.

A mod's replacement is checked against OpenReliant's own shader instead (checkReplacement, checkLink), as Replacements describes. A variant of the device shader isn't checked: it is OpenReliant's own with a mod's functions in it.

Shader cache

Improvement: shader_cache.zig keeps each compiled shader in the game folder's cache/shaders, one file for each shader, named by the SHA-256 of the shader's name: crt/crt.frag for a post effect, variant device.glsl cel-shading/bands.glsl cel-shading/ink.glsl for a variant of the device shader, or retro/device.glsl vertex for a stage of a mod's replacement. A file holds the 56-byte header every cache file starts with (cache_file.zig), then the SPIR-V's size, the SPIR-V and the Metal source:

Offset Size Field
0 4 ORSH
4 4 The layout's version, 3
8 32 The key: SHA-256 of the compiler's pinned versions (deps/shader-compiler/build.zig.zon), its wrapper (shader_compiler.cpp), and the shader: its kind, its stage, its definitions, and each part's name and source
40 8 XxHash3 of the rest of the file
48 8 The size of the rest of the file in bytes
56 4 The SPIR-V's size in bytes
60 The SPIR-V, then the Metal source

A file whose key doesn't match, whose sizes don't add up or whose hash is wrong is ignored, and the shader compiles again and replaces it. Files are written to a temporary file first and then renamed into place. A file that can't be written is logged, and the shader is used anyway. Shaders that don't compile aren't kept.

Post effects

Improvement: the mods' post effects (gpu/effects.zig, Post effects). Each effect is a fragment shader that draws the screen-wide triangle of the bloom's passes, reading the frame through the screen sampler (gpu.drawScreenPass, gpu.screenPassPipeline). As a frame is put on the screen (Gpu.compose):

  1. The bloom and the tone finish the frame, as before.
  2. The effects of the stage before the display draw over it, in order.
  3. The display and the menus are drawn over what they left.
  4. The effects of the stage after the display draw over that.

The passes take turns writing into two targets of the finished frame's format and size, so each reads what the last wrote (source). Each also reads the finished frame before any effect (frame_image), and gets the frame's size, the seconds passed and its four parameters in its uniform block. What the last pass wrote is what the window shows and a screenshot saves. A frame without passes is drawn as before, without the extra targets. An effect's pipeline is made the first time it draws; if it can't be made, the error is logged and the effect is left out. A frame draws at most 64 passes, as many as the scripts can register.

Replacements

Improvement: a mod's device.glsl, bloom.glsl or shadow.glsl replaces OpenReliant's own shader (gpu/programs.zig, whole_shaders.zig, Replacing OpenReliant's shaders). As OpenReliant starts, while MOD EFFECTS is on, the last mod in the load order with the file has both its stages compiled through the shader cache. OpenReliant's own source is compiled the same way, and the replacement is checked against it by reflection:

  • It uses only textures and uniform blocks at the set and binding where OpenReliant's has one, a texture of the same type, and a block no larger than OpenReliant's, which is what the GPU fills. It uses no other kind of resource and no specialization constants.
  • Its inputs are inputs OpenReliant's reads, at the same location and of the same type, so a vertex stage reads only the attributes the pipelines give.
  • It writes every output OpenReliant's writes, alike, since the next stage or a post effect reads them, and no built-in output OpenReliant's doesn't. A fragment stage writes no other outputs.
  • Its fragment stage reads only what its vertex stage writes, alike.

The GPU then draws with the replacement's code in place of OpenReliant's (Gpu.init, programs.chosen), with the same resources bound. A replacement that doesn't compile or doesn't fit is logged and left out. A replaced device.glsl is also the template of the variants with mods' functions in them (variants.Template).

Surface and lighting functions

Improvement: the mods' surface and lighting functions (gpu/variants.zig, Surface and lighting functions). Each is a variant of the device's fragment shader, shaders/device.glsl or a mod's replacement for it, compiled with a mod's lighting function (MOD_LIGHTING), its surface function (MOD_SURFACE) or both, inserted where the line // mod_functions stands. variants.Template cuts the shader there and where it includes colour.glsl, so the parts need no copying. Without either definition the shader is OpenReliant's own, which make shaders compiles as before.

  • Each variant has an id, and each pipeline carries the variant it draws with in its key (PipelineKey.variant), 0 for OpenReliant's own. A draw takes its object's surface function (srapiext.MeshObject.surface), else its texture's (srtexture.Image.surface), else, where it is lit for each pixel and drawn into the scene, Variants.every's. A draw without one takes Variants.base, the variant with the lighting function where one draws (Variants.pick).
  • A surface function's variant has the function's id, and the lighting function's variant without a surface function has the lighting function's. The driver compiles each with the lighting function that draws, and again when that changes (mod_shaders.zig). Replacing or removing a variant releases its pipelines (Gpu.removeVariant).
  • The variants read one more uniform block (variants.Uniforms, set 3, binding 3): the surface function's parameters, which each run pushes where they change, the lighting function's, and the seconds passed. Runs with different parameters don't join. The Surface a function changes also gives the size in pixels of the frame it draws into (frameSize), which gl_FragCoord counts in, from the frame's uniforms.
  • A surface function registered as see-through (srtexture.ModSurface.see_through) makes a surface the game draws solid blend by the alpha the function sets: the driver puts it aside with the blended draws, which are sorted farthest first (seeThrough, drawDeferred), and the device takes its alpha from the texel as the function leaves it (device.State.see_through). Unless it is registered not to (writes_depth), it still writes depth, so that of two such surfaces that cut into each other the nearer hides the other.
  • While MOD EFFECTS is off (Gpu.mod_effects), every draw takes variant 0 and no post effect draws.

Improvements

Deliberate differences from the original, each marked Improvement where it is made. The settings screen's VIDEO turns the GPU device's on and off as the game plays, but for 16-bit colour and linear light, which take effect at the next start (Video):

  • The view is unstretched on any screen: the factor across keeps pixels square, and a wider screen shows more at the sides (Camera).
  • The background picture is scaled to the screen's height, keeping its proportions, and centred horizontally, where the original driver stretches it over the screen (srd3d.backgroundEdges). The original pictures, which are 4:3, cover the screen the same way, and a wider picture from a mod fills a wider window (Modding).
  • The driver tests a sorted polygon's triangles against the sun with the polygon's own corners, where the original uses indices left over from the last list it drew. Only a solid polygon hides the sun: what is blended, such as a canopy's glass, lets it through.
  • A vertex with no counterpart in the next level of detail morphs toward itself, where the original reads whatever lies before that level's vertices.
  • The finer levels of detail reach eight times as far as the original's (srapi.Context.finer), so that a ship keeps its finest mesh until it is far off. Its last level still ends where the original's does at the top of the graphic detail's range, depths divided by 3 at HIGH (game.main.detailDivisor), and the ship leaves sight there.
  • A frame may draw 200000 vertices and as many polygons, ten times the original's 19999 (srapi.budget), in a mission and in the loadout alike. The layers are drawn from what went into them last, so once the budget is spent the objects that went in first are left out, and those are the ships' parts. OpenReliant keeps up to 4000 burning bits where the original keeps 500, and a view full of them and of a split's bodies takes the original's budget, so a wreck's parts would vanish while they are in view. A mod's ship can also have a larger model than the original's whole budget. --original keeps this budget, since the original's would leave such a ship out without a word.
  • The GPU device draws at the display's own resolution, or at a share of it that the settings' RESOLUTION or --size chooses (Video), with four samples a pixel, where the original drew one.
  • It filters textures trilinearly, sixteen times anisotropic, where the original sampled bilinearly from the nearest level, and magnifies them with a Catmull-Rom filter, which keeps the small textures sharp up close. A texture marked smooth (srtexture.Image.Magnify) is magnified with a cubic B-spline instead, from four bilinear taps, which neither rings nor sharpens: the nebulae's, whose 256 texels span 90 degrees of the sky, so that a texel covers about eight pixels at 1080p and Catmull-Rom showed their grid (Backdrop). One marked edge-adaptive is magnified with FSR 1's upscale, EASU, from AMD's FidelityFX Super Resolution 1.0 (MIT licence, its notice in the shader): twelve texels about the pixel, weighed by a Lanczos-shaped kernel stretched along the edge they show, then held between the nearest four so that it does not ring. The movies' frames are, which it keeps sharp at several times their size without steps on their edges (Movies). A glyph of the menus' fonts, marked coverage, is drawn from its coverage, the grey its levels are drawn in, as stored whatever the lighting (in linear light the texture comes decoded, and the shader encodes it again): each of its pixels a square of its own coverage, the step from one to the next eased over a pixel of the frame rather than a texel. The front end's and the pause menu's text so keeps the fonts' own shapes and greys at the window's size, crisp, in the colour it is drawn in, without a filter's blur or the nearest texel's uneven steps; at a whole multiple of a font's size, its pixels come out as they are (Front end). The menus' text is drawn from outline fonts by default, each glyph's pixels on the frame's (Outline fonts), so this draws the bitmap fonts' glyphs: under --bitmap-fonts, and those an outline font has no glyph for.
  • The frame's bright parts, its lights, flares and the sun, bleed a little light into what stands around them, as a camera does. What passes a threshold is taken into a half-size target, blurred along each axis in turn and added back, so that a light reads as a light rather than as a bright texel. The original drew none.
  • It lights each pixel of a lit mesh with the game's directional and point lights, where the original lit each vertex and interpolated the colours across each polygon. A hull of few polygons shades smoothly, and a point light falls off across a face rather than only between its corners. The pipeline still works out each vertex's own colour, ambient lights and baked colours, and hands the driver the vertex's normal in the camera's frame as well. The driver hands the device the frame's directional and point lights, also in the camera's frame, and the shader adds them with mesh_light's sums, to the normal interpolated and made unit length again, and holds each channel to 1. The shader takes up to 64 lights a frame, which keeps them within the 4 KiB of uniform data SDL's Vulkan device binds: the directional lights first, then the point lights nearest the camera. The pipeline adds any others to each vertex, as the original adds them all. A planet with an atmosphere takes the key light a little way past its terminator, as its air carries the sun round into the night side: the shader takes the light's cosine from -0.25 rather than 0, scaled back to the whole light facing it (srapiext.MeshObject.soft_terminator, Backdrop). The software device lights each vertex. --no-pixel-lighting turns it off.
  • Every shot a gun fires casts its light, where the original lit only the latest two of the player's shots and the latest two of everyone else's, so that sustained fire lights the hulls it passes (Guns). The shader's 64 nearest point lights take them per pixel and the pipeline adds any past that to each vertex. --few-shot-lights restores the original's two.
  • The steady lights of ships and stations shine on what stands near as real lights, as the blinking ones do, where the original baked them into the vertex colours of their own model (Rendering). --baked-lights restores the bake.
  • A gun's muzzle flash casts a light while it lasts, of its flares' own colour, so that each shot lights the hull round the gun, where the original's flash lit nothing. The turrets' guns flash too, blue or orange as their shots are, where the original gave them none (Guns).
  • An explosion's burning bits take the lights a ship takes, one of each pair, where the original let every light reach them, both suns and both fills, which washed them out (Effects).
  • Explosions are fuller: there is room for 128 fireballs where the original kept 30, each one's light moves with it and starts 50% brighter, shockwaves' rings are round where the original's were octagons, and bursts far off are not thinned, into a pool of 4000 particles (Effects). Burning bits stay until their place is needed, with room for 4000, where the original keeps up to 500 for about 20 seconds (Effects). A fireball's frames fade into each other, and every effect is drawn between the ticks as the ships are (Effects).
  • It lights and filters in linear light, where the original worked on the colours as they are encoded, gamma and all. The textures are kept as sRGB, so sampling, filtering and the mipmaps decode them, and the device decodes the lights' colours before they are added up, a point light's intensity after its colour. Each pixel's directional and point lights are added up and multiplied by the texture in linear light, the key light's share falling off as light does, and encoded again as the pixel is written. A fill light, the nebula's glow, keeps the original's falloff, and the vertex's own colour, its ambient and baked light, is added to the encoded texture as the original added it: the lights' colours were chosen against that neutral floor, and without it the side of a ship away from the sun glows with the nebula. What is blended, the game's glows, particles, fireballs and shields, is blended on the encoded colours, as the effects were made to be, into a frame of floats, 32 bits a pixel where the GPU draws into R11G11B10_UFLOAT and 64 otherwise, so that what is stacked past white is kept. The bloom's last pass, which runs whether the frame blooms or not, eases each channel past 0.8 toward 1 rather than clipping it, so a fireball's heart still goes white but what lies around it keeps its shading, and dithers the frame; the bloom takes the eased colours too. --gamma-space restores the original's way, as --original and 16-bit colour do.
  • It shades the material maps of mod textures for each pixel (Modding). The texture table loads the maps next to a mod's picture (srtexture.Image.Maps), and the GPU device keeps them in separate arrays next to each texture array, at the texture's layer: the normal and material maps as linear values, the emissive maps as sRGB, as the textures are. Each vertex says which of its texture's maps are used. A normal map tilts the pixel's normal in the texture's frame on the surface, which is worked out from how the pixel's position and texture coordinates change across the screen, so the meshes need no tangents; this is only done for textures with a normal map. OpenGL normal maps have green pointing to the top of the picture, against the direction of v. The tilt also darkens the vertex colour, the ambient light, by the cosine of the angle between the tilted normal and the surface normal, so details show where only ambient light reaches. Each mipmap level stores in alpha the length of the averaged normal, 1 at the finest level (srtexture.Content.normal), and the material's roughness is widened by the normals' spread with Toksvig's method ("Mipmapping Normal Maps", 2005): GGX alpha squared plus twice the variance (1 - length) / length, at most 1. That way details smaller than a pixel make the surface look rougher instead of making its highlights sparkle. A material map adds each light's highlight: GGX microfacets, Smith shadowing with Schlick's approximation, and Schlick's Fresnel, with 4 percent of the light reflected head-on except for metal, which reflects its base colour. All of it is in the diffuse light's units, which include the factor of pi like Lambert's term. A fill light, the nebula's glow, gives no highlight. It's a glow across much of the sky, which a rough surface reflects in much the same way as it scatters diffuse light, so it adds the material's reflectance times its light, with the original's falloff, and metal takes on the nebula's tint like paint does. Metal gets no diffuse light, and occlusion darkens the vertex colour, the ambient light. An emissive map's colour, read decoded from sRGB as the textures are, is encoded again and added to the pixel once it is lit, where a mod's surface function can change it (Surface.glow); the loadout's holograms leave it out. The driver skips the highlight pass for textures whose material map the device shades (Driver.drawsSecond). --no-materials turns the maps off, as --original does.
  • Materials reflect their surroundings. Each frame, once a texture with a material map has been uploaded, and when lighting is in linear light, the pipeline draws the objects the scene marks as reflected (srcore.Scene.reflected), the sky dome and the nebula patch, into a cube map with 256x256 faces, each a 90 degree square view along one of the camera's axes (srcore.cube_faces), and the device generates its mipmaps. A material's pixel samples it along the reflected view direction, from the sharpest level for a smooth surface to the blurriest for the roughest, weighted by Brian Karis's fit of how much a surface reflects for its reflectance, roughness and viewing angle. So glass and paint reflect at glancing angles, and metal everywhere. The sun isn't drawn into the cube map, since its reflection is the key light's highlight, and with reflections on, a fill light adds nothing to metal, since the nebula already shows in the reflection.
  • It draws in 32-bit colour, where the original drew in 16 bits, and dithers that too, which costs nothing and keeps a dark gradient, such as the nebula or a light's falloff, from banding. --original restores the original's look: 16-bit colour, dithered, into a 16-bit buffer where the GPU has one, with a 16-bit depth buffer, one sample a pixel, bilinear filtering, lighting each vertex, the levels of detail changing as near as the original's, lights from the latest shots only, muzzle flashes that light nothing and none from the turrets, an explosion's debris lit by every light, its fireballs, rings, particles and burning bits as few, plain and brief as the original's, the Uber Explode as coarse, unlit and tied to the frame rate as the original's, no shadows, and light worked out on encoded colours.

Scene objects of kinds 5 and 6

Nothing in the shipped game draws a line or a ball, and nothing makes one:

  • SR_driver_init fills every entry of the payload's device table from +0x3C to +0x84 and leaves +0x5C and +0x60 null. Those two are what sr_draw_layers (0x004C7960) calls for kinds 5 and 6, so an object of either kind would call through a null pointer.
  • The linker pulled one function out of srline.cpp and one out of srballs.cpp, line_pipe (0x004CE830) and balls_pipe (0x004CE7B0), each reached only from the switch in sr_draw_layers. Whatever creates such an object was never linked in, because nothing calls it.

The weapons' tracers are ordinary mesh objects and sprite sets (Guns), not these.

Both pipes begin with SR_object_rotate, which leaves the object's transform in the camera's frame at +0x7C (three rows) and +0xA0 (the place), and both work as the mesh pipeline does.

line_pipe walks the object's vertices, +0xB4 of them at +0xD4, four floats each. It transforms each into the camera's frame and gives it the same outcode the mesh pipeline uses: 0x10 for a vertex in front of the near plane, then 1 and 2 for a vertex outside the view's left and right at its depth, 4 and 8 for below and above. A vertex inside keeps 1/z as its fourth float, and its place on the screen, x and y over z, goes to +0xD8 as a pair of floats. The codes go to +0xCC, one byte a vertex. Then each of the +0xB8 segments at +0xDC, 28 bytes each, holding the two vertices it joins at +0x04 and +0x08, is marked at +0x10 when both ends are off the same side, which rejects it.

balls_pipe is a point with a size. A depth below the near plane returns 0x100, which sr_draw_layers takes as nothing to draw. Otherwise 1/z scales the size at +0xF8 into the radius at +0xD0 and the camera-space place into the screen place at +0xC4 and +0xC8, and the drawn record at +0xC0 points back at the object.

Not yet ported

  • The software renderer, srddraw.dll, and the software renderer's sky dome.
  • The mesh sets model_load builds for cloaking.
  • Hanging each part from its parent part's node (object_link_parts), which leaves every part where it is, and the moment of inertia object_bounds sums.
  • What node_draw draws for the cloak and for nodes of kind 6.

Edit this page on GitHub. The documentation is under CC BY-SA 4.0.