Aktuelt

Ett uvanlig kveldsprosjekt

Skrevet av Jarle Adolfsen | 03.02.2026

3D motoren ingen ba om, men som jeg måtte lage!

Det finnes mange måter å bli en bedre utvikler på.
Bøker, kurs, sertifiseringer og arkitekturdiagrammer.

Og så finnes det den andre veien:
å lage et prosjekt som er helt unødvendig stort, teknisk krevende, og som ingen egentlig har bedt deg om å lage.

Dette er historien om et slikt prosjekt.

Et bevisst valg: et tribute til Virus

Veldig tidlig i prosjektet tok jeg et ganske klart valg. Dette skulle bli et tribute-spill til Virus på Atari ST/Amiga/Amstrad fra 1987.

Ikke en remake.
Ikke gjenbruk av kode, assets eller data.
Alt er laget fra bunnen av.

Det eneste jeg har lånt er idéen bak spillet, kontrollene og gameplay-følelsen.

Og jo mer man ser på Virus i dag, jo mer absurd imponerende er det. Dette var verdens første hjemmecomputer-spill med ekte fylt 3D. Ikke wireframe(streker)! Med partikler, ganske avansert fysikk, skygger og detaljer overalt. Det er til og med fisk som svømmer i vannet.

Det tok mange år før det kom et like imponerende 3D-spill igjen på hjemmemaskiner.

Grafisk sett vil jeg faktisk påstå at Virus er mer imponerende enn Elite, når man tar maskinvaren i betraktning. Det er rett og slett utrolig imponerende å se hva de fikk til i 1987. Vil gjerne minne om at dette er 39 år siden!

Litt personlig bakgrunn

Jeg har jobbet med programvare i mange år, både som utvikler og som medbygger av produkter og selskaper. Jeg var blant annet med å starte Link Mobility, og har jobbet mye i skjæringspunktet mellom teknologi, skalerbare systemer og forretning. I dag er jeg CTO i bspoke, hvor jeg jobber med fag, tekniske valg, arkitektur, kunder og produktutvikling.

Parallelt med dette har jeg en forhistorie som demo-utvikler fra 90-tallet, blant annet i Imagina. Den gangen betydde utvikling å jobbe tett på maskinvaren, ofte i Motorola 68000 assembler, med ekstremt begrensede ressurser og full kontroll over hver eneste clock cycle. (Motorola 68000 er en ekstremt mye brukt CPU som var brukt i stort sett alle hjemme computere på 80 og 90 tallet.)

I dag har man kanskje blitt litt late i forhold til ressursbruk og effektiv programmering. Smarte effektive løsninger er ikke så relevant lenger.

Den kombinasjonen har preget måten jeg jobber på i dag. På den ene siden moderne systemer som skal fungere stabilt i stor skala. På den andre siden en nesten irriterende trang til å vite hvordan ting faktisk fungerer når du fjerner lag på lag med abstraksjon.

Dette prosjektet er på mange måter et forsøk på å komme litt tilbake dit. Ikke bare av nostalgi, men fordi det er utrolig lærerikt å bygge noe der du selv må ta ansvar for hele pipelinen, fra matematikken i bunnen til det som faktisk vises på skjermen.

Hvorfor ikke Unity eller Unreal?

Et spørsmål som ofte dukker opp er hvorfor jeg ikke bare brukte Unity eller Unreal.

Svaret er egentlig ganske enkelt. Jeg savner retro-følelsen.

Ikke retro som i lav oppløsning eller filtre, men følelsen av å forstå hele pipelinen. Å vite hvor hver frame kommer fra. Å kjenne begrensningene, og noen ganger irritere seg litt over dem.

Unity og Unreal er fantastiske verktøy. Men de gir meg ikke helt den samme følelsen som spill som Virus gjorde.

Målet her var derfor å lage en motor som gir den samme opplevelsen, men uten de ekstreme begrensningene fra 80-tallet.

I min spillmotor kan vi i dag ha noen tusen flater samtidig, stabil framerate og ordentlig lyd og musikk. Noe som, helt ærlig, var ganske smertefullt i originalen. Det er sjarmerende i ettertid, men ikke nødvendigvis noe man savner.

Det er et kompromiss. Retro-mentalitet kombinert med moderne ytelse.

Moderne verktøy med bevisste begrensninger

Denne gangen ønsket jeg å bruke moderne teknologi, men med vilje legge på begrensninger.

Valget falt på .NET Core og WPF.

(WPF er Microsoft sitt hoved-rammeverk for Windows baserte desktop applikasjoner)

Et valg som på mange måter er feil. WPF er ikke laget for spill og ikke laget for sanntids-3D. Det er omtrent som å ta på seg fjellsko for å løpe 100 meter.

Men dette er også noe av deg som motiverer meg, faktisk få til ett spillbart spill i ett verktøy som krever at du virkelig graver litt etter raskere og mer effektive måter å gjøre ting på.

Ganske tidlig ble det tydelig at å få WPF til å levere stabil og høy framerate krever langt mer enn å bare tegne litt på skjermen. Første forsøk var å rendre til bitmap og vise det i UI-et. Det fungerte helt fint, helt til det ikke gjorde det lenger.

Når verden ble større og antall trekanter økte, traff denne tilnærmingen en ganske solid digital vegg.

Pr i dag består en verden av opptil 100 000 objekter og ca 500 - 1500 flater på skjermen om gangen.

Løsningen ble mer pragmatisk. WPF har faktisk Direct3D-tilgang under panseret. Jeg endte derfor med en hybridløsning der all matematikk, world handling og kollisjon skjer på CPU, mens selve trekant-tegningen gjøres hardware-akselerert via WPF.

Det var først da spillmotoren kom tilbake til stabile 60 FPS, og kveldsprosjektet sluttet å føles som et eksperiment som var dømt til å tape!

Utviklingsforløpet i korte trekk

Første versjon var ren wireframe. Ett objekt som kunne roteres med piltaster. Målet var ganske beskjedent: å se om dette i det hele tatt var mulig.

Neste versjon fikk fylte flater, hidden face-logikk og enkel shading basert på dybde og vinkel. Jeg laget også en STL-import for å teste mer komplekse objekter, og begynte så smått å stole på at dette faktisk kunne bli noe.

Deretter kom surface-objektet, crash detection og starten på en egen partikkel- og fysikkmotor. Det fungerte, men frameraten falt til rundt 30 FPS, og optimismen fikk seg en liten knekk.

Så fulgte en større refaktorering. Ryddigere arkitektur, tydeligere ansvar og hardware-tegning av trekanter. Resultatet var tilbake på 60 FPS, og prosjektet var plutselig morsomt igjen.

Når prosjektet fikk navn

Etter å ha holdt min første talk om prosjektet på en Bspoke-konferanse ble én ting tydelig. Dette var ikke lenger et lite eksperiment man kunne omtale som “noe jeg tester litt”.

Spillet fikk navnet The Omega Strain.
Spillmotoren fikk navnet Retro Mesh.

Når noe får navn, har det en tendens til å bli værende.

 

Render pipeline i Retro Mesh

Render-pipelinen er bevisst enkel. All 3D-matematikk gjøres før rendering. Rendereren får ferdig projiserte 2D-trekanter og gjør én ting: tegner dem så raskt og forutsigbart som mulig.

Det innebærer dybdesortering, tidlig klipping, dybdebasert shading og ganske aggressiv caching av farger og pensler.

Rendereren er ikke spesielt smart. Den er bare veldig konsekvent, og det er ofte en undervurdert egenskap.

Bare tårnet i bildet under har 150-160 flater, like mye som hele Virus på Atari ST.

Lyd: når stillheten blir et problem

Da grafikken begynte å sitte, ble mangelen på lyd veldig tydelig. Spill uten lyd føles tomme, og det tok ikke lang tid før det ble umulig å overse.

Så jeg bygde et eget lydbibliotek basert på NAudio, med miksing av alle aktive lyder, støtte for one-shots og segmentert looping for musikk.

Score ble laget med Suno, lydeffekter hentet fra Freesound og deretter finjustert og tilpasset spillet.

Plutselig føltes verden litt mer levende.

Har dessverre ingen musiker tilgjengelig, Suno for the rescue!

 

CrashDetection: den minst glamorøse delen

CrashDetection er den delen jeg har brukt mest tid på. Jeg vil anslå 200 til 300 timer, og dette er mange timer fylt med frustrasjon og nitidig tolking av logger.

Ikke fordi matematikken i seg selv er ekstremt avansert, men fordi alt annet må være riktig samtidig. Koordinatsystemer, world offsets, rotasjon, ytelse og logging må stemme, ellers ender du opp med kollisjoner som nesten fungerer. Og “nesten” er ofte verre enn “ikke i det hele tatt”.

CrashDetection kjøres etter at objektene er flyttet og rotert, men før rendering. På dette tidspunktet er all world-state ferdig oppdatert, og kollisjonssystemet kan jobbe med stabile data. Fokus er på å sjekke færrest mulig objektpar, men sjekke dem riktig.

Hvert objekt kan ha én eller flere crashboxer. Disse er definert lokalt på objektet, men brukes i world space under kollisjon. Noen crashboxer roteres sammen med objektet, andre gjør det ikke. For eksempel følger crashboxene på skip og fiender rotasjon, mens enkelte surface-baserte objekter og bakken selv bruker ikke-roterte bokser for å holde beregningene enklere og raskere.

Selve kollisjonstesten er bygget rundt Axis Aligned Bounding Boxes, AABB. Det betyr at hver crashbox først bygges opp som et sett world-koordinater, hvor minimum og maksimum på hver akse brukes til å lage en akselåst boks. Dette er en bevisst trade-off. AABB er ikke like presist som full OBB-kollisjon, men det er raskt, forutsigbart og lett å debugge. I praksis gir det mer enn god nok presisjon for denne typen spill.

Rotasjon håndteres før AABB-sjekken. For roterte crashboxer blir alle punkter først transformert til world space basert på objektets rotasjon og offset. Deretter bygges AABB-en rundt de ferdige world-punktene. På den måten kan selve kollisjonstesten være enkel, selv om geometrien er rotert.

En feilvurdering jeg gjorde underveis var å introdusere egne crashbox-offsets i tillegg til objektets vanlige offsets. Tanken var at dette skulle gi mer fleksibilitet og finjustering av kollisjon, og på papiret hørtes det ut som en god idé.

I praksis førte det til at rendring og crash detection opererte med litt forskjellige sannheter. Objekter kunne se riktige ut visuelt, men kollidere “ved siden av seg selv”. Forskjellen mellom det som ble tegnet og det som ble testet for kollisjon skapte mye forvirring, og gjorde logging og feilsøking unødvendig vanskelig.

Først da vi strømlinjeformet dette, og sørget for at både rendering og crash detection brukte samme offsets og samme world-koordinater, fikk systemet den forutsigbarheten det trengte. Det var et klassisk eksempel på at litt for mye fleksibilitet kan gjøre et system vanskeligere å forstå enn nødvendig.

Fra et objektorientert perspektiv var det også en viktig lærdom. Hvert objekt har ansvar for sine egne crashbox-data, world offsets og impact-status. Selve CrashDetection-systemet koordinerer prosessen, filtrerer bort irrelevante par tidlig og utfører testene. Når en kollisjon oppstår, settes resultatet tilbake på objektene i form av impact-retning og tilstand, slik at fysikk, partikler og gameplay kan reagere videre.

God logging var helt avgjørende for å få dette til å fungere. Ikke logging av alt, men målrettet logging av riktige ting, på riktige tidspunkt. Uten det hadde dette vært umulig å ferdigstille.

Kort sagt: CrashDetection er en øvelse i tålmodighet, struktur og disiplin. Og en veldig effektiv måte å lære seg ydmykhet på. Litt motvillig, men ganske grundig.

World handling og scener

For at dette skulle bli et faktisk spill, trengtes struktur. Retro Mesh har derfor en enkel scene manager.

En scene kan settes opp, resettes helt eller byttes ut. Reset betyr faktisk reset, med helt ny instans og ren state. Det er overraskende befriende, men tok lenger tid enn forventet (Som alt med dette prosjektet)

Scene1 bygger hele verden. Skipet først, deretter surface og kart, AI-objekter og surface-baserte objekter som tårn, trær og hus. Kameraet er i stor grad fast. Det er verden som beveger seg.

Det gir enklere kontroll, bedre kollisjon og en tydelig retro-følelse.

Hva jeg har lært så langt

Dette prosjektet har vært enda mer lærerikt enn først tenkt!

Jeg har lært ekstremt mye om ytelsesoptimalisering i WPF, og hvordan WPF faktisk renderer under panseret.
Jeg har blitt minnet på at et spill er et stort logisk problem, ikke én stor utfordring, men mange små som alle må stemme samtidig.
CrashDetection er frustrerende, og kanskje det største problemet i 3D-programmering.
God logging og gode verktøy er helt avgjørende hvis du faktisk vil komme i mål.
Begrensninger er ofte en fordel. De tvinger deg til å forstå mer enn du kanskje hadde tenkt.

Trust the process sier man jo, og med spillutvikling stemmer det ekstremt godt. Det å tilbringe titalls timer på små detaljer, fører frem til slutt. Frustrerende mens man holder på, veldig givende når det virker.

Status nå og veien videre

Per i dag fungerer rendering, spillmotor, crashdetection (trenger antagelig litt fintuning), scenehåndtering, audio og world handling veldig godt. Det er en litt uvant, men en veldig tilfredsstillende følelse.

Det er litt ting som mangler, det skal på flere objekter, flere partikler og ikke minst flere enemies. Det skal også lages ett system for skygger, det er en utfordring jeg gleder meg til.

Veldig snart vil vi være inne i den morsomme fasen. Gameplay, spill-AI og det som faktisk gjør spillet gøy å spille.

Planen er å utvikle ett foredrag og Workshop for NDC og liknende konferanser så se opp for det i 2026.

Koden ligger åpent på GitHub: https://github.com/bspokeJarle/3dExploration

Det hadde vært veldig moro om flere ønsker å bidra eller bare følge med. Her har du muligheten å være med å lage ett faktisk spill! Vil gjerne minne på at dette er ett kveldsprosjekt, skulle gjerne hatt mer tid til å kvalitetssikre kode og arkitektur, men om du vil bidra til å rydde opp optimalisere osv er det bare moro! Jeg er jo ingen gamedev heller, dette er jo ett moroprosjekt!

Målet på sikt er å få spillet ferdig og legge det på Steam. Når vi har kommet dit, er planen å se på en Unity-versjon, først og fremst for VR. Jeg tror dette spillet kan fungere veldig godt i VR.

Men én ting av gangen.
Dette er fortsatt et kveldsprosjekt. Bare et litt for ambisiøst et ;)