Werking tussen classes van Game Engine

Pagina: 1
Acties:

Onderwerpen


Acties:
  • 0 Henk 'm!

  • Mischa_NL
  • Registratie: Mei 2004
  • Laatst online: 01-02-2023
Ik ben al een aantal maanden bezig aan mijn game engine, en hij begint nu vorm te krijgen. Dat wil zeggen: ik ben verschillende componenten nu met elkaar aan het verbinden.

Een aantal dingen zijn zo goed als af of iig bruikbaar. Denk aan: shaders, materials, basic geometry, lighting, etc.

Materials zijn bomen die dmv een gui in elkaar gezet worden, gecombineerd met textures. Deze worden op runtime gecompiled naar hlsl, en vervolgens naar een xna.effect. Materials worden gekoppeld aan geometry. Dit is een ´unified´ systeem: alle materialen kunnen aan alle geometry gekoppeld worden. Ook genereerd de compiler automatisch de benodigdheden voor lichten.
Lighting ben ik een aantal dagen mee bezig. Tot dusver: multipass lighting. Ambient, Directional en Point lights. Verder heb ik tot dusver alleen een sphere en een plane geometry gemaakt. (meer heb ik op dit moment niet echt nodig!). Punt van verbetering: Meerdere lichten per pass. Maar dit staat niet hoog op de prioriteiten lijst op dit moment.
De scenegraph begint ook vorm te krijgen: Het enige wat hij doet is transformaties bijhouden. Ik heb er bewust voor gekozen de graph erg beperkt te houden in wat hij doet. Er zijn op dit moment pas een paar nodes: transform, geometry en light (Light is WIP). Alpha transparancy wordt bijgehouden in de material en NIET in de scenegraph.

Waar ik nu echter mee bezig ben is het koppelen van de scenegraph aan de renderer. Op het moment is er een Draw() functie in de scenegraph, die vervolgens alle geometry sorteert en een voor een de renderer instuurt. Op dit moment rendert hij alle lichten in de LightManager, ipv wat hij zou moeten: alle lichten in de scenegraph.


Echter, nu vraag ik me af wat slimmer is:

1. De world manager retrieved op de world.Draw() alle geometry en lichten van de scenegraph en stopt ze in de renderer. Deze sorteert vervolgens op alpa/opaque en doet wat magie in de 'culling tree' om vervolgens te vinden welke lichten per geometry gerenderd moeten worden.

2. Als de worldmanager SceneGraph.Draw() aanroept kijkt de scenegraph welke objecten getekend moeten worden en sorteert deze (zie boven). Vervolgens kijkt men in de culling tree en bepaalt welke lichten met welk object getekend moeten worden. De scenegraph set de material/geometry EN de benodigde lichten op de device, stuurt de transformation matrix mee (translation, rotation) en tekent de geometry.


Het dilemma is dus eigenlijk:
Is de scenegraph verantwoordelijk voor het aanroepen van de renderer, of is dit het werk van een world manager die alle geometry binnenhaalt (of in een flat list bewaard) en ze vervolgens laat tekenen door de renderer.

Zelf neig ik naar het eerste, omdat dit alles een stuk makkelijker maakt. De scenegraph zorgt voor het scenemanagment: visibility, sorteren, en naar de renderer sturern, terwijl de renderer alleen maar tekent wat hij binnenkrijgt.

De vragen:
Is het slim om de renderer en SceneGraph te scheiden? (lijkt me een ja)
Wat is de elegantste manier om de renderer te benaderen? Vanuit een world manager, of vanuit een scenegraph?
Waar zou een culling tree moeten komen? Is deze onderdeel van de scenegraph of van de world manager?
Wat is de slimste manier om lichten te cullen/vinden? Ik dacht aan boundingsphere, in de culling-tree, en vervolgens testen tegen geometry.

Een hoop vragen, ik hoop dat het enigsinds duidelijk is!

Acties:
  • 0 Henk 'm!

  • Guldan
  • Registratie: Juli 2002
  • Laatst online: 23:23

Guldan

Thee-Nerd

Waar ik nu echter mee bezig ben is het koppelen van de scenegraph aan de renderer. Op het moment is er een Draw() functie in de scenegraph, die vervolgens alle geometry sorteert en een voor een de renderer instuurt. Op dit moment rendert hij alle lichten in de LightManager, ipv wat hij zou moeten: alle lichten in de scenegraph
Dus als ik het zo lees heb je nu een lightManager en een Lightrenderer in een klasse. Je kan dit opdelen en de render methode in een interface knallen zodat je het volgende kan doen:

C#:
1
2
3
4
5
6
7
8
9
10
//initialiseer lightmanager
Lightmanager lm = new LightManager();

List<IRenderer> list= new List<IRenderer>();
list.Add((IRenderer) new LightRenderer(lm));

foreach(IRenderer r in list)
{
   r.Render();
}


Hierdoor worden ten eerste de renderers gescheiden en daarna op een generieke manier geimpleteerd. Hierdoor maakt het ook niet uit welke reneders je in de lijst stopt.

Ik heb eerlijk gezegd meer verstand van OO dan van gamedesign. Als meer je tips wil moet je eens zoeken op Designpatterns.

[ Voor 6% gewijzigd door Guldan op 24-08-2010 15:11 ]

You know, I used to think it was awful that life was so unfair. Then I thought, wouldn't it be much worse if life were fair, and all the terrible things that happen to us come because we actually deserve them?


Acties:
  • 0 Henk 'm!

  • Mischa_NL
  • Registratie: Mei 2004
  • Laatst online: 01-02-2023
Guldan schreef op dinsdag 24 augustus 2010 @ 15:10:
[...]


Dus als ik het zo lees heb je nu een lightManager en een Lightrenderer in een klasse. Je kan dit opdelen en de render methode in een interface knallen zodat je het volgende kan doen:

C#:
1
2
3
4
5
6
7
8
9
10
//initialiseer lightmanager
Lightmanager lm = new LightManager();

List<IRenderer> list= new List<IRenderer>();
list.Add((IRenderer) new LightRenderer(lm));

foreach(IRenderer r in list)
{
   r.Render();
}


Hierdoor worden ten eerste de renderers gescheiden en daarna op een generieke manier geimpleteerd. Hierdoor maakt het ook niet uit welke reneders je in de lijst stopt.

Ik heb eerlijk gezegd meer verstand van OO dan van gamedesign. Als meer je tips wil moet je eens zoeken op Designpatterns.
Het probleem zit hem niet in het scheiden van renderers, maar welke systemen welke taken moeten uitvoeren.

De scenegraph is vooralsnog het enige 'object' wat de renderer aanroept.
Echter hoe moet ik de renderer aanroepen?

Op dit moment sorteert de scenegraph alle sceneobjects.
Vervolgens set hij er een op de device en roept dan renderer.Draw aan.
Deze tekent dan en vervolgens set hij de volgende.

Probleem is dat een geometry object door verschillende lichten beinvloed wordt.
Nou lijkt hij mij verstandig dit in de scenegraph uit tezoeken en naast een setGeometry ook een SetLights functie aan de renderer toe te voegen.

Dan krijg je het volgende:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Foreach (GeometryNode node in DrawableNodes)
{
   If (node.Visible && node.Active)
   {
      node.Draw(renderer)
   }
}

node.Draw(Renderer r)
{
    r.SetGeometry(this.geometry);
    // en de vraag is, is dit hieronder een slimme methode
    r.SetLights(SceneCuller.FindLights(this));
}


Scenegraph marked nodes bij een update Active of inactive op basis van switch nodes (if distance > x, set active x set inactive y).
Moet de graph bij update ook de lights zoeken of dit pas doen wanneer ik render. Lastig lastig...

Acties:
  • 0 Henk 'm!

  • Laurens-R
  • Registratie: December 2002
  • Laatst online: 29-12-2024
Ook ik heb niet bepaald kaas gegeten van game engines, maar je scenegraph, etc bevatten in feite de state van je gamewereld. Dus ik zou daar helemaal geen OnDraw routines of andere aanspreek/manupulatiepunten voor de renderer in aanbrengen. Vanuit een OOP perspectief denk ik nl nog steeds dat elke class z'n eigen verantwoordelijkheid zou moeten hebben en dat je met tussentijdse updates/aanroepingen tussen de classes e.d. een kruisbestuiving van verantwoordelijkheden creeert.

Ik zou gewoon het overkoepelende systeem laten uitvogelen waar de scenegraph etc staat en hoe deze bij de renderer moet komen. Deze moet ook de renderer dan aanroepen om ook daadwerkelijk te gaan renderen. Ik neem aan dat wanneer je in de renderer aankomt dat je niks meer zou willen wijzigen in je scenegraph e.d. omdat deze tegen die tijd al helemaal verwerkt zou moeten zijn. Maar goed, zoals ik al zei; heb niet veel ervaring in de game-engine wereld dus wellicht dat ik je (en de princiepes achter scenegraphs etc) verkeerd begrepen heb ;)

Zo blijft de renderer lekker renderen en scenegraph, etc verantwoordelijke voor het houden van alle data over je wereld.

edit; net je tussentijdse antwoord gelezen; wat aanpassingen gemaakt.

[ Voor 48% gewijzigd door Laurens-R op 24-08-2010 15:40 ]


Acties:
  • 0 Henk 'm!

  • Mischa_NL
  • Registratie: Mei 2004
  • Laatst online: 01-02-2023
EvilB2k schreef op dinsdag 24 augustus 2010 @ 15:29:
Ook ik heb niet bepaald kaas gegeten van game engines, maar je scenegraph, etc bevatten in feite de state van je gamewereld. Dus ik zou daar helemaal geen OnDraw routines of andere aanspreek/manupulatiepunten voor de renderer in aanbrengen. Vanuit een OOP perspectief denk ik nl nog steeds dat elke class z'n eigen verantwoordelijkheid zou moeten hebben en dat je met tussentijdse updates/aanroepingen tussen de classes e.d. een kruisbestuiving van verantwoordelijkheden creeert.

Ik zou gewoon het overkoepelende systeem laten uitvogelen waar de scenegraph etc staat en hoe deze bij de renderer moet komen. Deze moet ook de renderer dan aanroepen om ook daadwerkelijk te gaan renderen. Ik neem aan dat wanneer je in de renderer aankomt dat je niks meer zou willen wijzigen in je scenegraph e.d. omdat deze tegen die tijd al helemaal verwerkt zou moeten zijn. Maar goed, zoals ik al zei; heb niet veel ervaring in de game-engine wereld dus wellicht dat ik je (en de princiepes achter scenegraphs etc) verkeerd begrepen heb ;)

Zo blijft de renderer lekker renderen en scenegraph, etc verantwoordelijke voor het houden van alle data over je wereld.

edit; net je tussentijdse antwoord gelezen; wat aanpassingen gemaakt.
De WorldManager is de alles-overkoepelende class waarin alle andere classes staan om je world te createn.
In principe zou je dus zonder world kunnen, en het zelf af kunnen handelen (als bepaalde methods niet internal zouden zijn).

De scenegraph is onderdeel van de world, en staat dus een stapje dichterbij de hardware. Deze houdt de states van alle nodes bij. States zijn echter zeer simpel:
isActive = true als de node actief is (en hij dus getekend moet worden)
isVisible = true als de geometry-node visible is. Isvisible kan niet op geometry gedaan worden omdat deze in object space staat, de geo-node heeft de world transform (positie)
World = position, rotation
On traverse van de scenegraph worden de states van nodes geupdate. Dus: isvisible, isactive en de rotatie/translatie.
Alle drawables staan ook in een flat list. Deze kun je dus doorlopen, om de isActive && isVisible nodes eruit te halen.

Ik zal wat code posten:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
    // stukje uit de world manager

    public class World
    {

        public void Update()
        {
            SceneGraph.Update();
        }

        public void Draw()
        {
            RenderManager.Begin();

            SceneGraph.Draw(RenderManager);

            RenderManager.End();
        }
    }


    // de scenegraph.Draw()
    public class SceneGraph
    {
        internal void Draw(Renderer renderer)
        {
            // do sorting and other stuff!
            DrawList.Sort(delegate(SceneNodes.iDrawableNode p1, SceneNodes.iDrawableNode p2) { return p1.GetGeometry().Material.Id.CompareTo(p2.GetGeometry().Material.Id); });
            foreach (SceneNodes.iDrawableNode geometrynode in DrawList)
            {
                geometrynode.Draw(renderer);
            }
        }
    }

//node.draw
        public class Geometry : Node, iUpdatableNode, iDrawableNode
        {
            private iDrawableObject geometry;

            public void Draw(Renderer renderer)
            {
                // call the draw method of the geometry!
                renderer.SetGeometry(geometry, this.DeviceState);
                renderer.Draw();
            }
        }


Ik vraag me dus concreet af of dit een logische manier is.

EDIT//

Voor de duidelijkheid: de scenegraph handelt alle states af tussen geometry. Ik denk dus dat ik maar voor de volgende oplossing ga:

De scenegraph heeft culling tree's (een voor static en een voor dynamic objects of iets dergelijks) en deze regelt wat er naar de renderer gaat.
Wil je geen gebruik maken van de scenegraph dan moet je voor elke geometry een State aanmaken, en ze los naar de renderer sturen. Je zult dan dus ook zelf culling af moeten handelen. Geburik je de scenegraph dan handelt deze culling af. Ook zorgt de graph voor culling van lichten. Gebruik je hem dus niet zul je ook dat zelf af moeten handelen.

De cullin tree is dus dan geen onderdeel van de world manager, maar van de scenegraph.

De renderer krijgt dan de volgende functies:
SetMaterial(material) <- alleen een materiaal setten
SetGeometry(geometry) <- geometry + materiaal van geometry setten.
SetLights(lights[]) <- zet lichten die hij moet meenemen in de rendering
De scenegraph zoekt uit welke material,geometry in combinatie met welke lichten getekend moeten worden.

[ Voor 13% gewijzigd door Mischa_NL op 24-08-2010 16:58 ]


Acties:
  • 0 Henk 'm!

  • Gorion3
  • Registratie: Februari 2005
  • Laatst online: 24-08 08:28

Gorion3

ABC++

Inprincipe zijn er 2 soorten rendering mogelijkheden, monolitisch en verdeeld. Eerste vorm houdt in dat je alle objecten zich registeren (vertices, texture coords etc) bij de renderer (of in jouw geval de SceneGraph/SceneManager) en de renderer deze renderd. Andere is dat een referentie naar het object wordt geregistreerd en de renderer deze aanroept ( obj.Draw(); ). Verschil is trouwens dat de daadwerkelijke render calls dus op 1 plek staan of verspreid zijn.

Voor opengl kies ik zelf altijd voor de eerste variant, maar bij xna weet ik het nog niet zeker, vooral omdat ik nog niet weet hoe ik alles ga renderen, vooral omdat je dat schrale spritebatch gebeuren moet gebruiken (wat perfect is, maar ik kan er niet mee omgaan ;))

En als antwoord op jouw vraag, moet voordat je gaat renderen de scenegraph niet worden doorlopen? Lijkt me wel, dus zou ik daar ook het renderen doen, als je een structuur hebt zoals een octree/quadtree hebt kan je het ook makkelijker recursief maken :)

[ Voor 5% gewijzigd door Gorion3 op 25-08-2010 09:15 ]

Awesomenauts! Swords & Soldiers


Acties:
  • 0 Henk 'm!

  • Mischa_NL
  • Registratie: Mei 2004
  • Laatst online: 01-02-2023
Gorion3 schreef op woensdag 25 augustus 2010 @ 09:14:
Inprincipe zijn er 2 soorten rendering mogelijkheden, monolitisch en verdeeld. Eerste vorm houdt in dat je alle objecten zich registeren (vertices, texture coords etc) bij de renderer (of in jouw geval de SceneGraph/SceneManager) en de renderer deze renderd. Andere is dat een referentie naar het object wordt geregistreerd en de renderer deze aanroept ( obj.Draw(); ). Verschil is trouwens dat de daadwerkelijke render calls dus op 1 plek staan of verspreid zijn.

Voor opengl kies ik zelf altijd voor de eerste variant, maar bij xna weet ik het nog niet zeker, vooral omdat ik nog niet weet hoe ik alles ga renderen, vooral omdat je dat schrale spritebatch gebeuren moet gebruiken (wat perfect is, maar ik kan er niet mee omgaan ;))

En als antwoord op jouw vraag, moet voordat je gaat renderen de scenegraph niet worden doorlopen? Lijkt me wel, dus zou ik daar ook het renderen doen, als je een structuur hebt zoals een octree/quadtree hebt kan je het ook makkelijker recursief maken :)
Scenegraph doorlopen op update, deze maakt lijsten met objecten die getekend moeten worden + met welke properties.
World.Draw() roept Scenegraph.Draw() aan.
De scenegraph loop de objects door:
code:
1
2
3
4
5
6
foreach geometrynode geo in list
{
    renderer.SetGeometry(geo.geometry);
    renderer.SetState(geo.worldstate);
    renderer.SetLights(geo.lights);
    renderer.Draw()

En tada, alles wordt getekend.

De cullingtree (die onderdeel is van de scenegraph) word op scenegraph.Update() aangeroepen, en culled lights en geometry, en koppelt geometry en lights.
Pagina: 1