News:

Shadow Casting / Shadow Projection demo  (20th May 2025)

Main Menu

PolyWOLF – A Wolfenstein 3D Engine for PlayBASIC

Member







PolyWOLF – A Wolfenstein 3D Engine for PlayBASIC (BLOG)

🧱 Introduction

Step back into the early '90s, where textured walls and corridor shooters ruled the screen. With PolyWOLF, we're bringing that classic Wolfenstein 3D vibe into the PlayBASIC world — only this time, the walls aren't raycasted, they're hand-crafted polygons mapped directly onto the screen.

PolyWOLF is a pseudo-3D engine built entirely in PlayBASIC. It doesn't rely on raycasting techniques but instead renders each wall and floor segment as affine-mapped textured polygons, drawing inspiration from early 3D engines like the one behind DOOM, but without perspective-correct mapping. The result is a clean, fast, and distinctly retro feel.


🎯 Project Goals

  -  Recreate the iconic Wolfenstein 3D-style gameplay and look using PlayBASIC

  -  Build a flexible engine using affine texture-mapped polygons

  -  Optimize for speed, simplicity, and nostalgic aesthetics

  -  Provide a starting point for custom games or engine experiments in PlayBASIC



🧩 Core Features

  -  Affine Texture Mapping:  True to early 90s game rendering, PolyWOLF uses flat texture projection onto polygons (TextureTri & TextureQuad) in such a way to avoid most of classic warped-yet-beautiful look of that area.

  -  2.5D Design: While the map rendering is technically 2D under the hood, PolyWOLF includes wupport for 3D objects within the 2.5 scene. 

  -  Low-Spec Friendly: Copy base is largely compatible with all revisions of PlayBASIC, ensuring it's compatible with a large cross section of low speced computers.



⚙️ How It Works

PolyWOLF's engine avoids raycasting entirely. Instead, it treats each wall or floor segment as a set of polygons that are manually transformed and projected onto the screen using affine math. The process:

  -  Converts map data into world-space polygons.

  -  Transforms polygons based on the player's position and view angle.

  -  Sorts geometry by depth.

  -  Renders the textured polygon directly onto the screen.


  This approach has its quirks — such as potential texture distortion given the use of affine mapping — but that's exactly what gives the engine its authentic retro charm.



🔧 Development Notes

  -  Language: 100% PlayBASIC

  -  Textures: 256x256 tile-based walls, floors and 3d textured objects

  -  Object Format: loads ASCII format direct x meshes.

  -  Editor: None - Level data is hand-coded into the source.





📦 Downloads

    Looking to try out the PolyWOLF engine or explore the source code? Grab the latest builds and resources below.

    🔽 Latest Release

    PolyWOLF Demo v005
    📁 Includes: PlayBASIC source + textures
    📅 Release Date: April 2025
    🧰 Requirements: PlayBASIC v1.65+



    PolyWOLF Demo v001/2 & 3 (Yet Another Wolfenstein 3D Demo)
    📁 Includes: PlayBASIC source + textures
    📅 Release Date: March 2025
    🧰 Requirements: PlayBASIC v1.64+





Member

#1
🎮 [PlayBASIC] Wolf 3D Demo - Revision 4 Now with Mip Mapping & Texture Optimization! 🚀

Hey fellow coders!

I just uploaded **Revision 4** of the Wolfenstein 3D-style demo, and this update is all about squeezing out more performance and pushing PlayBASIC further than most would expect!

Here's what's new in this version:


🔧 New Features in Rev 4
  - ✅ **Backface clipping** for environment walls and floors 
  - ✅ A brand new **texture library** with support for **mip mapping** 
  - ✅ Textures now **auto-adjust resolution based on distance** 
  - ✅ Much **lower rendering cost** for far-away objects 
  - ✅ Clean integration using standard PlayBASIC image handling


🧠 What is Mip Mapping (and why should you care)?

Each texture now has multiple versions (mip levels) at decreasing resolutions:
  - e.g., 128x128 → 64x64 → 32x32 → 16x16 
  - The engine selects the appropriate texture size depending on how far the object is from the camera.

This means:
    - 🧠 **Less memory bandwidth used**
    - ⚡ **Fewer cache misses**
    - 🎯 **Higher performance**, especially in scenes with lots of geometry

  I've color-coded the mip levels so you can visually see them in action in the demo.


📊 Performance Gains

  Even while capturing video (which knocks off ~10-15 FPS on my setup), the update keeps things smooth—no more big frame drops when the scene fills up!

  This feature is a **must-have** for larger scenes and brings PlayBASIC closer to what you'd expect from old-school engines like Doom or Wolf3D.


📽� Watch the Full Demo Video:



Let me know what you think—or if you'd like to see a breakdown of how the mip level system was coded. 
This update makes the engine far more scalable and a lot more fun to tinker with!

Cheers, 



**#PlayBASIC #GameDev #RetroEngine #Wolf3D #Raycasting #MipMapping #SoftwareRendering #TextureOptimization**


Member

🎮 PolyWOLF  V0.06 - Mip mapping 🚀

  This revision further brings Mip Mapping to floor and wall texture mapping, further squeezing out more rendering performance. 

🔧 New Features in Rev 6
  - ✅ Added Mip Mapping to Floor rendering
  - ✅ Added Mip Mapping to Wall rendering





Member

#3
PolyWOLF V0.07 Update

  Welcome back PlayBASIC coders! In this update, we're checking out Polywolf Revision 7. This version introduces a bunch of new features and optimizations, including:

  - New floor and wall textures
  - Support for different object types and render styles (e.g., filled polygons)
  - New object loading system with an ASY file loader (basic Wavefront OBJ support)
  - New functions: LoadObject, LoadTexture, DeleteObject, and CloneObject
  -  Faster object handling with cloning — load an object once, then quickly create copies without reloading from disk
  -  Improved workflow for dynamically creating and managing objects like bullets or particles

You'll see how the system now makes it easier to load, clone, and manage objects efficiently within  PlayBasic projects.


 


  Download

    Download PolyWolf V0.07


Member

#4
PolyWolf V0.08 - Billboarding / Sprites

Welcome back, PlayBasic coders! In **Polywolf Revision 8**, we're introducing billboarding — a technique that allows 2D sprites to always face the camera in a 3D space. This feature opens up new possibilities for blending 2D and 3D elements, giving scenes more variety and flexibility.

In this update:
- Billboarding support added via textured quads
- Simple sprite-based characters (like a mech and Mario) placed into the 3D scene
- Discussion of **MD2 mesh support** for low-poly models
- Exploration of draw modes like alpha blending and subtraction
- Techniques for dynamic sprite rendering and distance-based tinting
- Insight into handling environments without Z-buffering using painter's sorting
- Challenges around rendering parameters and possible future optimizations

This build continues the classic **Wolfenstein 3D-style engine** approach, while introducing more advanced rendering tricks using polygons and billboards.

Thanks for tuning in — see you in the next update!




Download

    - Download Source Code



Member

 PolyWolf V0.09 / V0.10 - Add frame support
   
   A quick update, rounded out the loading support for MD2's by including the loadPCX.   Those models tend expect a certain file structure given they're from Quake.  Some models seems to include texture names others don't, so that's annoying.   The library just try's to guess..  There's probably a better way --  you can just load the texture direectly.

  The other thing I've been adding is support for animation frames in the objects library.   The frames are loaded by the loader, but there's no support for frames in the Objects library.   Frames are just pre-baked versions of the vertex data. Much like frame based animation in 2d with pre-rendered cells.   It's a simple solution that worked well for the time and will make such simple works appear more interesting.





   29th Apr 2025 -4/5 May/2025 - Revision V0.09

- Added MD2 Loader code into the object library
- Wrote and added LoadPCXIMage(Filename$) function
- DeleteTexture(index) returns status of the delete
- LoadTexture(Filename$)  will create a default texture if it can't
find the filename provided





Member

PolyWolf V0.10 - Vertex Frame Animations Example

  Starting to made headway with vertex/frame animations.  Adding frames has meant rearranging the original Objects.pba library to support the concept of multiple vertex buffers - so this was a lot of small moving parts to tweak into submission.   But it's finally working today. 

  Adding that has meant breaking existing object loaders though, but that shouldn't be too much drama to get working again.   

 In this demo, each object just cycles through the different vertex buffers. 

 

Member

PolyWolf - V0.10 - Lets just throw a bunch of objects in scene and see what we get.. A good laugh is what you get ! :) 

#lowpoly #retro #softwarerendering #coding #gamedev #2d #3d #retrodev

Member

PolyWOLF V0.10 – Animation Support and Structural Improvements


  In this update, we explore the major addition to *PolyWOLF V0.10*:  *animation support* for 3D models. The demo showcases various characters—including classic dragons, Mario, and mech units—all rendered using **software-based 2D polygon rendering**, without hardware acceleration or Z-buffering.

The update introduces **vertex animation** support through changes to the object structure and loading system. While previous versions supported a single vertex and face buffer, this version expands the object library to handle *multiple animation frames*, each with its own vertex buffer. This enables *pre-baked animation* (as seen in MD2 models), similar to frame-based 2D animation.

The internal system now tracks a **current frame** and supports dynamic allocation of vertex buffers per frame. A simple frame playback system is included, allowing animations to cycle based on naming conventions (e.g., `run1`, `run2`, ..., `run10`).

Key highlights:

  * Added frame buffer support to each object.
  * New array-like dynamic frame structure within objects. 
  * Simple vertex poke system for editing vertex data.
  * Trivial animation sequencer based on frame naming.
  * Example characters perform different actions in a perspective-correct 2.5D scene.

  This update lays the foundation for further improvements such as collision detection and more advanced animation control, all while keeping things efficient and lightweight for PlayBASIC's runtime environment.






  PolyWolf Previous Episodes
  https://www.youtube.com/watch?v=G1Vwcgtn7w0&ab_channel=PlayBasic
  https://www.youtube.com/watch?v=JiBfgFQ4WRo&ab_channel=PlayBasic
  https://www.youtube.com/watch?v=snfoD71ZxfI&ab_channel=PlayBasic
  https://www.youtube.com/watch?v=xnDsZxvNxbQ&ab_channel=PlayBasic



Download

    - Download PolyWolf V0.10 Source Code



Member

PolyWOLF V0.12 – Load/Save Custom Object Format - LerpObjectAnimation


  The release expands the object.pba with the addition of the LoadObject / Saveobject binary format.  These functions give us a binary format for the object, that's easily loaded into the library rather than parsing legacy text base object formats. 

  Another addition is the  LerpObjectAnimation(ObjectIndex,AnimationNAME$ ,Steps)
function. This function lets the user insert pre-calculated interpolated vertex frames into the animations within the object. What it does is step through the existing animation.  For each pair frames from the original we can insert a user defined number of Steps  between these frames.  The more steps,  the more frames the smoother the animation.


Download
     
  Download PolyWolf V0.12
 


Member

just wondering how you are doing the z sorting?

i did the floors and roof first by raycasting.
then the walls by manually sorting them by z depth and drawing furthest to closest order.

you not only have floors & roof, but walls with 3D objects correctly clipping on them.
can you explain your method for these?

i haven't gone through the code line by line, which will take a while, so would like if you could explain the methods you used.

i find manually sorting z depth is a fair bit of code, but if you have a better method that is good. i know there is a z sort command but that only sorts a one dimension array. what would be good if it could carry extra data with it, like a 2 dimension array where it just sorts the first dimension but also carries with it the second dimension (but doesn't sort the second dimension data).

that way you could say have the z data in the first dimension pos, which gets sorted, but have the object number in the second dimension pos that it carries along with the first dimension. so after the first dimension gets sorted by z depth i can then refer to the second dimension object number it is attached to.
that way i can sort the z buffer and then render out the objects in the correct order.

thanks for any advice, steve.
It's easy to start a program, but harder to finish it...

I think that means i am getting old and get side tracked too easy.

Member

#11
Quotejust wondering how you are doing the z sorting?

i did the floors and roof first by raycasting.
then the walls by manually sorting them by z depth and drawing furthest to closest order.

you not only have floors & roof, but walls with 3D objects correctly clipping on them.
can you explain your method for these?


  The drawing order is bellow.  Floors/Ceilings -> then walls + objects


PlayBASIC Code: [Select]
function Render_scene(Camera)

// Fill the display the far depth colour, faded
cls rgbfade(Scene.depthcolour ,25)

// Compute visibiity of map cords
UpdateMapVisibility(Player.X,Player.Z,Player.Angle#)

// Build the floor vertex buffer - locatized to the player(camera)
FloorVertexCount,FloorPolygonCount =Build_Floor_Map_MipMapped(Player.X#,Player.Z#)

// rotate the vertex data and store those rotations into
RotateVerts(Vertex_Cache#(),RotatedVertex_Cache#(),FloorVertexCOUNT,0,Player.Angle#,0)

// Draw Floors + Ceilings
Draw_Floors_MipMapped(FloorPolygonCount)


// Build the vertex / polygon lists from the Walls
WallVertex,WallPolygons = Build_Walls(Player.X#,Player.Z#)

// rotate the vertex data
RotateVerts(Vertex_Cache#(),RotatedVertex_Cache#(),WallVertex,0,Player.Angle#,0)

// draw the walls and object to the scene buffer
// and let the drawcamera sort the polygons for us
Capturetoscene
clsscene
// Render walls ?
if Scene.DrawWalls=true
Draw_Walls_MipMapped(WallPolygons)
endif

// render objects
Draw_All_Objects(Player.X#,Player.Z#,Player.Angle#)

drawcamera Camera

// draw the stats
Draw_STATS(FloorVertexCount,FloorPolygonCount,WallVertex,WallPolygons)

EndFunction


 


  For objects there's basically this
PlayBASIC Code: [Select]
function Draw_All_Objects(PlayerX#,PlayerZ#,PlayerAngle#)

// Some stat's about the objects being drawn
Scene.Objects.InView =0
Scene.Objects.VertexCount =0
Scene.Objects.FaceCount =0

// Run through and pre-compute if the player can see these
ComputeObjectsVisiblity(PlayerX#,PlayerZ#,PlayerAngle#)


dim Obj as t3D_OBJECT pointer
for index=1 to getarrayelements(Objects())
if Objects(index)=0 then continue

Obj=OBJECTS(index).t3D_Object
if Obj.InView=false then continue

select Obj.ObjectTYPE
case ObjectTYPE_BILLBOARD
Draw_Object_BILLBOARD_TO_Camera(Obj, PlayerX#,PlayerZ#,PlayerAngle#)

case ObjectTYPE_MODEL
Draw_Object_Model_TO_Camera(Obj, PlayerX#,PlayerZ#,PlayerAngle#)
EndSelect

next

Endfunction





  Object rendering..  with a bunch of debug code in it

PlayBASIC Code: [Select]
    // ------------------------------------------------------------------------
// ------------------------------------------------------------------------
// ------------------------------------------------------------------------
// ------------------------------------------------------------------------

function Draw_Object_Model_TO_Camera(Obj as t3D_OBJECT pointer, PlayerX#,PlayerZ#,PlayerAngle#)

Clipping_DEPTH = Scene.Clipping_Depth

Distance#=getdistance2d(Obj.position.x#,Obj.position.z#,playerx#,playerz#)
if Distance#>Clipping_DEPTH then ExitFunction

Scene.Objects.InView ++


// rotate objects local vertex buffer
UpdateObject(Obj.INDEX)


// Objects_RotatedVertex_Cache#
makearray ObjectsRotatedVertexBuffer#()

// Get handle to this array
ObjectsRotatedVertexBuffer#()=Obj.RotatedVertex.Handle

Tilt# =0
turn# =playerangle#
roll# =0
VertexCOUNT =obj.rotatedvertex.count

ObjX# =obj.position.x-PlayerX#
ObjY# =obj.position.y
ObjZ# =obj.position.z-PlayerZ#

// localize the mess to the player/camera
Offset=0
for lp=0 to (VertexCOUNT-1)

Offset1 = Offset+1
Offset2 = Offset+2

// objects rotated / global cordinates
gx#=objx#+ObjectsRotatedVertexBuffer#(offset)
gy#=objy#+ObjectsRotatedVertexBuffer#(offset1)
gz#=objz#+ObjectsRotatedVertexBuffer#(offset2)

// localize these to the players position
Objects_RotatedVertex_Cache#(offset) =gx#
Objects_RotatedVertex_Cache#(offset1) =gy#
Objects_RotatedVertex_Cache#(offset2) =gz#

offset+=Object_Vertex_MODULO

next

// Add these vertex to the
Scene.Objects.VertexCount +=VertexCOUNT

RotateVerts(Objects_RotatedVertex_Cache#(),Objects_RotatedVertex_Cache#(),VertexCOUNT,Tilt#,turn#,roll#)

// ----------------------------------------------
// Get The Texture for this objects depth in the scene
// ----------------------------------------------
ObjectTEXTURE = getTextureMippedImage(Obj.texture.index,Distance#,Clipping_DEPTH)
if ObjectTexture>0 and getimagestatus(ObjectTexture)
tw=getimagewidth(ObjectTexture)
th=getimageheight(ObjectTexture)
endif

ThisRGB =GetSceneDepthColour(0)
DepthColour =GetSceneDepthColour(1)


Local TransparentFLAG = Obj.Transparent

// define a pointer a mesh gave structure
dim Face as tObject_MeshFace pointer

// Get the objects Mesh base pointer
local MeshPtr = Obj.Mesh.Ptr

if MeshPtr=0
#print "Object #"+Str$(Obj.index)+" Missing Face Mesh "

ExitFunction
endif

// How many faces are in this mesh ??
FaceCount = obj.Mesh.Count

DrawMETHOD =rgbr(Scene.RenderMOde)
DrawMode_FILLED = DrawMethod=0
DrawMode_TEXTURE = DrawMethod and 1
DrawMode_GOURAUD = DrawMethod and 2

// force filled polygon if texture doesn't exists
if ObjectTexture=0
DrawMode_FILLED = true
Login required to view complete source code



  All it does is read the transformed Z's of the triangle, averages the three  Z's and the set's that as the camera depth. 

  When the camera is rendered all the capturd polygons are sorted far to near and drawn in that order..  aka painters


i find manually sorting z depth is a fair bit of code, but if you have a better method that is good. i know there is a z sort command but that only sorts a one dimension array. what would be good if it could carry extra data with it, like a 2 dimension array where it just sorts the first dimension but also carries with it the second dimension (but doesn't sort the second dimension data).

that way you could say have the z data in the first dimension pos, which gets sorted, but have the object number in the second dimension pos that it carries along with the first dimension. so after the first dimension gets sorted by z depth i can then refer to the second dimension object number it is attached to.
that way i can sort the z buffer and then render out the objects in the correct order.


    The trick is to never move anything, as even the quickest sorts are still generalize solutions. But this is not a general problem, we have known limits such as Z's range  >=0 out the clipping depth.  So rather than brute force some array / swapping elements, with lots of potential iterations ( quicksort / merge sort) and memory thrashing, just to get the data in a serialized fashion..  Why not just have a secondary array that holds the order. 

    Then run through ( during / after creation) and drop a bread crumb about this polygon into the sort array.  So basically keep the index / pointer to the polygon in the big heap with the z's.  We use the Z's to look up what's at this depth.   


   Example: 
   Z-Sorting in Software Without Traditional Sorting – PlayBASIC Demo