castle-doom3
Doom 3 (id Tech 4) levels, materials, md5 models and sounds rendered and played with Castle Game Engine (Object Pascal)
- Active · last change 11-4-2026
- ★ 2 on GitHub
README
Castle Doom 3
A port of Doom 3 (id Tech 4) to the Castle Game Engine (Object Pascal / Free Pascal), the sibling of mariuz/castle-quake1, mariuz/castle-quake2 and mariuz/castle-doom.
It reads Doom 3's own file formats at runtime: PK4 archives, .proc render geometry, .map entities, .mtr materials with diffuse / normal / specular maps, .md5mesh / .md5anim skeletal models, .lwo / .ase static models, .cm collision models, entity, model, skin and sound declarations. Doom 3's data is not redistributable, so a small original test level in real Doom 3 formats (data/base/castle_doom3_test.pk4, generated by tools/make_test_pk4.py) is bundled. Point the game at a Doom 3 install to load its maps.

- 📖 Architecture & Engine Mapping
- 🗺️ Doom 3 Comparison & Roadmap
- 📝 Changelog · Releases (Windows, Linux)
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Features (and the Castle Game Engine parts they use)
| Doom 3 | Castle Game Engine |
|---|---|
PK4 archives (base/pak000.pk4 ...), loose files, later sources override earlier ones |
Doom3Files: own ZIP central-directory reader with inflate (ZStream), served through the d3fs: URL protocol (RegisterUrlProtocol) |
.proc areas (_area0...), brush entity models, portals, area BSP |
X3D nodes built in code: TIndexedTriangleSetNode with TNormalNode and TTextureCoordinateNode, one shape per area and material in one TCastleScene with PreciseCollisions |
.mtr materials: diffusemap / bumpmap / specularmap (or blend diffusemap stages), translucent, twoSided, nonsolid, alphaTest, blended stages (blend add, blend blend, blend filter) |
Phong TMaterialNode with DiffuseTexture, NormalTexture (CGE computes tangents) and SpecularTexture; unlit TUnlitMaterialNode + TBlendModeNode for glows and decals |
Image programs: heightmap(), addnormals(), makeIntensity()..., Doom 3's normal map convention |
Doom3Images evaluates them and serves the result through the d3img: protocol (green channel flipped for CGE's tangent space) |
.tga, dds/*.dds textures |
CGE's TGA and DDS loaders via d3fs: URLs, shared through CGE's texture cache |
Per-pixel lights (light entities: light_radius, light_center, _color, projected light_target / light_up / light_right / light_start / light_end, ambientLight shaders, start_off) with Doom 3's light volumes: no distance attenuation, a box (point lights) or frustum (projected lights) mapped to the light's texture space like lightProject, projection and falloff shaping |
X3D TPointLightNodes with Global and a Radius (CGE gives each light only to shapes it reaches) and a per-light Effect plugging PLUG_light_scale, reading the fragment's world position from castle_vertex_eye and the EyeWorldSpace shader library (Doom3LightEffects) |
| The flashlight | a TCastleSpotLight child of the camera, designed in the editor |
.md5mesh / .md5anim skeletal models, model decls (mesh, anim, offset, inherit), skins |
Doom3Md5: CPU skinning into a TCoordinateNode each frame, interpolated frames, one TCastleScene per instance |
.lwo (LightWave LWO2: PNTS, POLS, PTAG, TAGS, VMAP / VMAD UVs) and .ase (3ds Max ASCII: sub-materials, *BITMAP → material) static models for func_static, props, light fixtures, items, LWO doors |
Doom3StaticModels: Doom 3's coordinate swaps and face flips, Doom-style smooth normals (shared position and UV), one TCastleScene per entity; map geometry (func_*) collides unless noclipmodel |
.cm collision models (vertices, signed edges, nodes, polygons with materials, brushes) with Doom 3's invisible playerclip brushes; contents from material surface parameters (playerclip, monsterclip, nonsolid, water, trigger...) |
Doom3Collision: the world model's player-blocking polygons as an invisible scene (Visible := False, PreciseCollisions) that the player and monster sight collide with; the render scene stays pickable for shots |
GUIs: .gui windowDef trees (rect, backcolor, matcolor, background, bordercolor / bordersize, text with gui:: state from the entity's gui_* keys, textscale, textalign, forecolor, visible) on guisurf surfaces (entity, entity2, entity3 or a path) |
Doom3Gui: a software renderer into a 640 x 480 TRGBAlphaImage with CGE's built-in UI font glyphs, served by the d3gui: URL protocol and shown with an unlit texture |
Material stages: texture matrices (scroll / translate, scale, centerScale, shear, rotate) and color registers (rgb, alpha...) on surfaces with expressions, so screens scroll and panels pulse; cameraCubeMap / cubeMap skies (with texgen skybox); every plain stage (glows over lit walls), deform sprite, glass with heat haze (heatHaze* programs reading _currentRender), cube map reflections (texgen reflect) |
TextureTransformMatrix3D nodes and unlit material colors updated every frame (UpdateAnimatedSurfaces); the sky's six images on a TCastleBackground, sky surfaces left undrawn; more stages are drawn on the same triangles with their own unlit appearance and a DepthMode "lequal" depth test; sprites are Billboard quads; heat haze is an Effect sampling a ScreenTexture (the screen behind it) moved by the stage's normal map; reflections are a ComposedCubeMapTexture with reflection texture coordinates, added; other glass is a faint additive tint |
Stencil shadows (Doom 3 shadows every light; CGE renders shadow volumes for one) from brush entities, static models, md5 models (big ones that aren't closed through a box per joint) and the level's walls and pillars (its .cm brushes); noshadows materials and lights |
CGE shadow volumes (TCastleViewport.ShadowVolumes): each frame the strongest light at the player (in the player's area first) becomes the shadowVolumes / shadowVolumesMain light; the open area geometry only receives, the .cm brushes are rebuilt as closed convex casters 1 unit inside the walls, invisible (shadow-only casters); the .proc's precomputed volumes (shadowModel) cast for their light (r_prelightShadows); noshadows sets Appearance.shadowCaster; md5 meshes over r_md5ShadowTris (1500) triangles cast with their own triangles when they are closed (r_md5ShadowClosed), otherwise with closed boxes under per-joint MatrixTransforms |
Light shaders: stage color registers (red, green, blue, alpha, rgb, rgba, color, colored) with material expressions (time, parm0-11, global0-7, table[...], arithmetic, comparisons, && / ||) and table decls (snap, clamp): strobes, flickers, pulses; shaderParm0-11 |
Doom3Expressions (expression trees, idDeclTable lookups); TDoom3Map re-evaluates animated lights every frame and sets the X3D lights' Color / Intensity |
Portal visibility: the area BSP, interAreaPortals, doors closing portals, lights limited to the areas they reach |
Doom3Portals: portal flow from the camera's area through portal polygons clipped by the camera Frustum and narrowed per portal; one TCastleScene per area and the entities, md5 models and lights of hidden areas get Visible := False / IsOn := False (console r_usePortals, portals) |
entityDef with inherit |
merged key / value dictionaries (Doom3Decls) |
AAS navigation: the map's .aas48 (areas, BSP, reachabilities) routes monsters around walls and through doorways (Dijkstra over walk, walk-off-ledge and air reachabilities, steering to the next reachability); monsters open doors. Console aas [entity] shows areas and the route to the player |
Doom3Aas (TD3AasFile), TD3Game.RouteWaypoint |
Head looks: lookAt / lookAtEnemy turn the look_joint joints (waist, chest, neck...) towards the focus within look_min / look_max, easing at head_focus_rate; separate heads follow the body's neck (copy_joint). Console actors shows the look angles |
TD3Game.UpdateLook, TD3Md5Instance.SetJointMod / AddCopyJoint |
Splash damage (def_splash_damage: damage falling off to radius, blocked by walls, not after a direct hit), detonate_on_fuse; Bernie zombies (monster_zombie_bernie); monsters see through playerclip and nonsolid surfaces (sight is a trace on opaque geometry) |
TD3Game.SpawnProjectile / SplashDamage / CanSee |
Talking to characters: the use key in front of a character whose script allows it (setTalkState( TALK_OK )) makes the player its talk target (AI_TALK, $player1); the head plays its lip sync anims (talk_primary...) and their sound_voice frame commands; getHead, setTalkTarget, getTalkTarget, turnToEntity / distanceTo the player |
TD3Game.Use, TD3Actor.TalkState / TalkTarget |
Imps' and security zombies' AI scripts (monster_demon_imp, monster_zombie_security_pistol: pistol, shotgun, machine gun zombies): projectiles fired by the launch_missile frame commands (the monster's def_projectile: speed, gravity with an arc that reaches the player, fuse, attack_accuracy, projectile_spread, num_projectiles, damage from def_damage; fireballs as glowing balls); frame commands call and trigger |
TD3Projectile, TD3Game.LaunchProjectiles / UpdateProjectiles |
Zombies' AI scripts (monster_zombie and the objects derived from it): monster_base's waking (hide, trigger, ambush, sight), state_Combat: checkForEnemy (the player in sight and field of view), moveToEnemy (with collisions), faceEnemy, testMeleeAttack, melee attacks dealt by the animations' melee frame commands (damage from the def_damage entityDef), pain (AI_PAIN), death (state_Killed); anim variants (idle -> idle1..idle3) and sound* frame commands |
ScriptedMonsterObjects, TD3Game.ActorFrameCommand, TD3Md5Instance.ResolveAnim / OnFrameCommand |
Characters' AI scripts (scriptobject character, Mars City's people): the object's init, state functions (setState / setNextState) in the main thread, animation states (animState) in a thread per channel, playAnim / playCycle / idleAnim / animDone, walking to path_corners (moveToEntity, at the walk animation's speed), turnTo, waitAction / finishAction, callFunction (path commands: path_anim, path_cycleanim, path_wait...), AI_ACTIVATED / AI_FORWARD / AI_MOVE_DONE |
TD3Actor, TD3Game.SpawnActors / ActorEvent / UpdateActors |
Animated models: idAnimated (ships and props of cinematics: first frame of anim at spawn or a looping start_anim, plays when activated, cycle, num_anims sequences, auto_advance, wait, remove, fires its targets at the end), cinematic characters (cinematic 1: appear and play their anim once), heads (def_head at head_joint, playing the body's anim of the same name) |
TD3Game.SpawnAnimated / ActivateAnimated, TDoom3Map.BuildHead, TD3Md5Instance.SetFrame / JointTransform |
Cinematics: camera anims (.md5camera, idCameraAnim: cuts, old_origin offset, cycle, targets fired at the end), camera views (func_cameraview), trigger_fade and the scripts' fadeIn / fadeOut / fadeTo, info_player_teleport, Escape skips a cinematic (and the shots it chains to); lights fading in and out (fadeInLight / fadeOutLight); sounds started by scripts (startSoundShader, startSound, stopSound) |
Doom3Camera, TD3Game.StartCamera / CinematicView / FadeColor / TakeTeleport, applied by TViewPlay.UpdateCinematic |
Scripts: Doom 3's script language compiled and run (script/doom_main.script with everything it includes, and maps/<map>.script): functions, namespaces, objects, #define / #include, threads that wait / waitFor / waitFrame; the map's main, worldspawn's and triggers' call keys; events for triggering, lights (on / off, colors), doors, hide / show, entity keys, GUI parameters (setGuiParm redraws the entity's screens), bind and idMover's move / moveTo / rotateOnce / rotate / startSpline (func_splinemover curves) with time / speed / accelTime / decelTime |
Doom3Script (TD3ScriptVM: compiler to a stack machine, threads), TD3Game.ScriptEvent, TD3ScriptMover |
Entity activation: trigger_once / trigger_multiple / trigger_hurt touched by the player (volumes from the .cm; delay, wait, triggerFirst, noTouch), trigger_relay / trigger_count, target chains switching lights on / off, starting speakers, showing hidden entities and monsters, waking dormant monsters, opening / unlocking doors, func_remove |
TD3Game.Activate / ActivateTargets, a queue of delayed firings; lights through TDoom3Map.SetLightOn, hidden entities through TCastleTransform.Exists |
func_door (opens when you come near, movedir incl. up / down, lip, speed, time, wait, locked, snd_open / snd_close, travel from the rotated bounds, teams opening together, target_lock), func_static, func_rotating (start_on, toggled by activation), bind (brush entities and models following a mover), entityDef spawn arguments as defaults |
brush entity scenes moved by TD3Mover; TCastleSoundSource behaviors for door sounds |
speaker entities and sound shaders (.sndshd: files, minDistance / maxDistance in meters, volume in dB, looping) |
TCastleSound (ReferenceDistance, MaxDistance, linear distance model) on a TCastleSoundSource |
Reverb like dhewm3: the map's efxs/<map>.efx EAX reverb settings, chosen by the listener's area number, location (info_location, spread through portals up to closed doors and info_locationSeparators) or "default". Console reverb, s_useEAXReverb [0|1], soundinfo |
Doom3Efx, CGE's SoundEngine.Reverb (OpenAL EFX) |
| Player: 68 units eye height, 16 radius, walk 140 / run 220, 16-18 unit steps, crouch, jump | TCastleWalkNavigation (gravity, PreferredHeight, ClimbHeight, RunMultiplier, mouse look), collisions with the area scenes and brush entities |
Monsters (monster_* entityDefs with md5 models) |
a tiny idAI: sight checks with WorldSegmentCollision, chase with wall sliding, floor following with WorldRayCast, melee, death |
| HUD, console, menu | Doom 3's guis/hud.gui and guis/mainmenu.gui when the data has them; else TDoom3Hud, TDoom3Console (registered components) and a map list from the PK4s |
The bundled test level has two rooms joined by a portal and an auto-opening door, stairs to a balcony, a crate stack, LWO barrels, ASE computer terminals with scrolling screens, pulsing ceiling panels, a skylight onto a starry sky, a GUI door panel and a status screen, an invisible playerclip wall and a monster clip strip in its .cm, a spinning fan, ten lights (point, projected, ambient, a projected window pattern; flickering, pulsing and strobing light shaders), a looping hum, two animated md5 creatures that chase you, pain cries, blood blobs and a tunnel vision when hurt, berserk and invisibility powerups, stamina, crouching (C) under a crawl bar, a third person view of the player's body (pm_thirdPerson 1) wearing powerup skins, crouching, holding its gun and falling as a ragdoll, guns ejecting brass, and items to pick up (health, armor, ammo, a shotgun, a key); the player starts with fists and a pistol, scripted weapons with view models.
Working in the Castle Game Engine editor
- Views are designed, not hard-coded.
TViewPlayandTViewMenuloaddata/gameviewplay.castle-user-interfaceanddata/gameviewmenu.castle-user-interface; the viewport, camera (90° horizontal FOV),TCastleWalkNavigation, the flashlight and muzzle flash lights, HUD and console are published fields you can tweak in the editor. - Custom components (
editor_unitsinCastleEngineManifest.xml): Doom 3 Map (TDoom3Map), Doom 3 HUD, Doom 3 Console. Open the project in the editor and use Project → Restart Editor (With Custom Components). TDoom3Mapis aTCastleTransform: setMapName(testmap, or e.g.game/mars_city1with a Doom 3 install) and the level, its lights, brush entities and animated md5 models are built right in the editor viewport.SpawnModelstoggles the md5 models,LightScalescales all lights, read-onlyAreaCount,EntityCount,TriangleCount,LightCountandStaticModelCountshow map info.UseCollisionModel(default on) collides with the map's.cminstead of the render geometry.PortalCulling(default on) draws only what is visible through the portals in the game.
Controls
| Key | Action |
|---|---|
| W, A, S, D / Arrows | Move / strafe |
| Shift | Run |
| Mouse | Look |
| Left Mouse / Ctrl | Fire the weapon (restart the level when dead) |
| 1 ... 0 / Mouse wheel | Select a weapon (slot 0 = fists, 1 = pistol...) / the next or previous one |
| R | Reload |
| Space / C | Jump / Crouch |
| F | Flashlight |
| E | Use (open doors, talk) |
| Tab | The PDA screen (PDAs, emails, objectives; [ and ] switch PDAs) |
| ~ | Console (map <name>, maps, noclip, god, notarget, flashlight, kill, give [all|weapons|<weapon>|<stat> <amount>], inventory, weapon <slot>, weapnext, weapprev, reload, pm_bob [0|1], hurt <damage def> [amount] [up], hurtmonster <name> [damage def] [amount], decal <material> [size], guis, objectives, pda [name], stamina [value], pm_stamina <seconds>, pm_thirdPerson [0|1], pm_thirdPersonRange <units>, pm_thirdPersonDeath [0|1], crouch [0|1], ragdoll [0|1], g_showBrass [0|1], pos, setviewpos x y z [yaw], fps, lightscale <f>, decls, r_usePortals [0|1], r_lightVolumes [0|1], r_shadows [0|1], r_prelightShadows [0|1], r_shadowLights <n>, r_useScissor [0|1], r_useDepthBounds [0|1], r_shadowMaps [0|1], r_shadowMapSize <n>, r_taa [0|1], r_envProbes [0|1], r_envProbeBake, r_envProbeScale <f>, r_skipParticles [0|1], particles, portals, lights, soundinfo, reverb, s_useEAXReverb [0|1], quit, help) |
| F5 / F12 | FPS counter / screenshot |
| Escape | The menu over the paused game (close / Escape in it comes back); in the title menu Tab switches between the menu GUI and the map list |
Building and Running
Prerequisites: Castle Game Engine 7.0-alpha.3 or later (bundles FPC 3.2.2).
$env:PATH = "C:\castle-engine\bin;C:\castle-engine\tools\contrib\fpc\bin;" + $env:PATH
castle-engine compile --mode=release
.\castle-doom3.exeAudio needs OpenAL32.dll (from C:\castle-engine\bin) next to the exe; castle-engine package bundles it.
Game data
Sources are added in this order (later ones override earlier ones, like Doom 3's search path):
data/base/castle_doom3_test.pk4: the bundled test level (testmap).- A
base/directory next to the executable: its*.pk4files sorted by name (pak000.pk4...pak004.pk4), then its loose files. Withoutbase/, ademo/directory (the Doom 3 demo). -basepath <Doom 3 dir>(loads<dir>/base, or<dir>/demofor the demo),-game <mod dir>and-pk4 <file>.
The free Doom 3 demo
The Doom 3 demo (2004) has three maps of the full game, shortened: game/demo_mars_city1, game/demo_mc_underground and game/demo_mars_city2. dhewm3's install page links its downloads; the Linux one, doom3-linux-1.1.1286-demo.x86.run (485 MB), works on every system, as only its demo/demo00.pk4 (483,535,485 bytes, MD5 70c2c63ef1190158f1ebd6c255b22d8e) is needed. The .run file is a shell script followed by a .tar.gz: on Linux sh doom3-linux-1.1.1286-demo.x86.run --tar xf demo/ extracts it; elsewhere skip the script's first 373 lines and untar the rest. Then:
.\castle-doom3.exe -basepath "E:\Work\doom3-demo" +map game/demo_mars_city1
.\tools\smoke_maps.ps1 -BasePath "E:\Work\doom3-demo" # load each map, report time, stats, warningsLike the full game, the demo data is id Software's: never commit it.
Doom 3 BFG Edition's binary formats (.bimage, .bproc, .bcm) are not supported; use the original 2004 game data.
Command line
.\castle-doom3.exe +map testmap
.\castle-doom3.exe -basepath "C:\Games\Doom 3" +map game/mars_city1
.\castle-doom3.exe --autotest testmap test_output\shot --demo "W:2,S,G:200 0 0,A:0,W:2,S,F,N:0,W:0.5,X,S,Q"
.\castle-doom3.exe --autotest MENU test_output\shot # title screen screenshot
.\castle-doom3.exe --autotest MENU test_output\shot --demo "W:1,S,C:320 240,Q" # drive the menu GUIDemo script actions (comma separated): W:sec wait, S screenshot, T:deg turn left, A:deg absolute Doom 3 yaw, P:deg pitch up, G:x y z go to Doom 3 coordinates (feet), GO:forward side seconds [run\|crouch] hold movement keys (and the run key or crouch), F flashlight, X fire, U use, Y god mode, N:i stand in front of monster i facing it, O:command console command (O:menu opens the in-game menu, O:menu x y clicks it, O:menu esc), L:map load map, Q quit. With --autotest MENU the script drives the title menu GUI: W:sec, S, C:x y click at GUI coordinates, K:esc, P:start / P:stop a sampling profile, Q. The log (%LOCALAPPDATA%\castle-doom3\castle-doom3.log) records every screenshot with the FPS, eye position, yaw, area, number of visible areas and the shadow casting light, and every shot (Shot:).
Tests
cd tests
castle-engine compile
.\castle-doom3-tests.exe # 296 checks of the lexer, PK4, decls, expressions, texture matrices, images, GUIs, .proc, .map, .cm, md5, lwo, ase parsers, light volumes and portal visibilityRegenerating the test level
python tools/make_test_pk4.py # writes data/base/castle_doom3_test.pk4References
- Doom 3 Source Code Review by Fabien Sanglard: the original Doom 3 engine, in six parts (overview, dmap, renderer, profiling, scripting, interviews).
- Doom 3 BFG Source Code Review by Fabien Sanglard: the BFG edition's code, in four parts (introduction, multi-threading, renderer, classic Doom integration).
- dhewm3: the maintained Doom 3 GPL source port, used for reference renders (see
docs/ROADMAP.md, "Features from dhewm3"). - RBDOOM-3-BFG: the modernized Doom 3 BFG (see
docs/ROADMAP.md, "Modern rendering and features from RBDOOM-3-BFG").
License
- Code and the generated test level: MIT License (matching Castle Game Engine and the sibling ports).
- No id Software data is included. Doom 3 is a registered trademark of id Software / ZeniMax Media.










