[Projekt] Prozedurales Universum

Hier könnt ihr euch selbst, eure Homepage, euren Entwicklerstammtisch, Termine oder eure Projekte vorstellen.
Forumsregeln
Bitte Präfixe benutzen. Das Präfix "[Projekt]" bewirkt die Aufnahme von Bildern aus den Beiträgen des Themenerstellers in den Showroom. Alle Bilder aus dem Thema Showroom erscheinen ebenfalls im Showroom auf der Frontpage. Es werden nur Bilder berücksichtigt, die entweder mit dem attachement- oder dem img-BBCode im Beitrag angezeigt werden.

Die Bildersammelfunktion muss manuell ausgeführt werden, die URL dazu und weitere Details zum Showroom sind hier zu finden.

This forum is primarily intended for German-language video game developers. Please don't post promotional information targeted at end users.
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

Schräge Sache. Ich frag mal NavyFish im Inovae Forum der hat recht viel mit Shadern gemacht, ggf ist der auf das gleiche Thema auch gestoßen. Wenn ich was rauskriege werde ichs hier per EDIT noch posten.

Ich habe zwischenzeitlich mit dem Quadtree Parsen und Splitten begonnen. Jetzt kommen die Details und die Sache beginnt schön auszusehen. Wie zu erwarten ist meine Grafikkarte nicht begeistert dass ich
auf Level 4 meines Quadtrees und 128x128 Planes auf ~50M Triangles komme, und mir die Reimplementierung der LODSphere und das Horizon Culling für später aufgehoben habe :lol:

Naja, aber langsam wirds was. Interessanterweise werden jetzt kreisförmige Artefakte sichtbar, wo ich noch am grübeln bin wo die herkommen. Meine erste Vermutung waren Precision Probleme im Noise Umfeld, aber
da hätte ich erwartet dass die eher irgendwo an den Rändern der Planes stattfinden und nicht, wie man auf dem Screenshot sieht, irgendwo südwestlich, und sich dann kreisförmig über den Planeten verteilen. Naja mal schauen.

Bild
Zuletzt geändert von sushbone am 26.12.2015, 12:22, insgesamt 1-mal geändert.
Benutzeravatar
Schrompf
Moderator
Beiträge: 4838
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: [Projekt] Prozedurales Universum

Beitrag von Schrompf »

Da die an Kanten und eher bei flachen Winkeln auftreffen, könnte es auch eine MipMap-Filterung sein, die da aus evtl. nicht existenten kleineren MipMap-Leveln Zufallsfarben ansaugt.

Sieht aber verdammt geil aus.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: [Projekt] Prozedurales Universum

Beitrag von Krishty »

Ist jetzt nicht so hilfreich, aber: Wenn es das Mip Mapping wäre, wären die Scheiben auf dem Bildschirm zentriert. Man kann aber sehen, dass die Fehler nach links unten "zeigen". Ich würde bei allem anfangen, was mit Polarkoordinaten arbeitet …
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

Es scheint mit höchster Warscheinlichkeit an der Simplex Noise Implementierung zu liegen.
Ich habe testweise mal die Normalisierung weggelassen um Quereffekte davon auszuschließen. Wenn ich testweise einen anderen Noise-Algorithmus verwende (Voronoi) treten diese Artefakte nicht mehr auf.

Bild

Bild

Man sieht auch ganz deutlich wenn man näher ranfliegt dass falsche Noise-Ergebnisse irgendwie zu prompten Höhenveränderungen führen, offensichtlich führen dichtbeiligende Koordinaten als Input des Noise zu plötzen Veränderungen im Ergebnis. Es lässt sich auch keine Regelmäßigkeit erkennen, fliegt man immer dichter ran treten diese Artefakte immer chaotischer auf.

Bild

Bild

Bild

Die Implementierung des FBM auf Basis des SimplexNoise könnte theoretisch es noch sein, ist aber auch eigentlich nichts ungewöhnliches.

Code: Alles auswählen

#ifdef NOISE_FBM
    //Good FBM values
    #define     NoiseOctaves    4
    #define     NoiseFrequency    0.0003    // 0.0003 for 6371 radius
    #define     NoiseAmplitude    1.0
    #define     NoiseLacunarity    2.418
    #define     NoiseGain        0.5
#endif

//An FBM [Fractal Brownian Motion] noise call
float FBM(float3 p, int octaves, float frequency, float amplitude, float lacunarity, float gain)
{
	float noise = 0.0f;           
	for (int i = 0; i < octaves; ++i)
	{
			noise += snoise(p.xyz * frequency) * amplitude;         
			frequency *= lacunarity;
			amplitude *= gain;
	}
    return noise;
}


//The kernel for this compute shader, each thread group contains a number of threads specified by numthreads(x,y,z)
//We lookup the the index into the flat array by using x + y * x_stride
//The position is calculated from the thread index and then the z component is shifted by the Wave function
//[numthreads(threadsPerGroup_X,threadsPerGroup_Y,1)]
[numthreads(1,1,1)]

// Do position calculations
void CSMain1 (uint3 id : SV_DispatchThreadID)
{
	...

	// Next we generate the noise value using the patch's 'real-world' coordinate (patchCoord)
	#ifdef NOISE_FBM
		float noise = FBM(patchCoord, NoiseOctaves + constants.nodeLevel, NoiseFrequency, NoiseAmplitude, 	NoiseLacunarity, NoiseGain);
	#elif defined NOISE_VORONOI
		float noise = Voronoi(patchCoord, NoiseOctaves, NoiseFrequency, NoiseAmplitude, NoiseLacunarity, NoiseGain);
	#elif defined NOISE_SIMPLEXNOISE
		float noise = SimplexNoise(patchCoord,NoiseFrequency,NoiseAmplitude);
	#endif

	...
}
Ggf. ist es eine gute Idee mal nach einer anderen Simplex Noise Lib zu suchen die ich im Compute Shader in Unity verwenden kann?!
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

Und tatsächlich lag es an der Simplex Noise Implementierung, es hatte sich dort ein Fehler eingeschlichen.
Mit der Anlehnung an die Original Ashima webgl-noise Simplex Noise Implementierung bei Github funktioniert es. :D

Bild

Hervorragend. Jetzt gehts noch darum einen Culling Bug auszumerzen (der Planet wird, warum auch immer, von Unity geculled sobald der Mittelpunkt des Planeten aus dem Frustum der Camera verschwindet),
dann kann ich weiter optimieren, d.h. LODSphere Optimierungen machen und Frustum Culling ergänzen. Danach gehts ggf. zum nächsten Thema, Wasseroberfläche oder Atmospheric Scattering, mal schauen.
Benutzeravatar
Schrompf
Moderator
Beiträge: 4838
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: [Projekt] Prozedurales Universum

Beitrag von Schrompf »

Sehr sehr cool! Weiter so!
sushbone hat geschrieben:Jetzt gehts noch darum einen Culling Bug auszumerzen (der Planet wird, warum auch immer, von Unity geculled sobald der Mittelpunkt des Planeten aus dem Frustum der Camera verschwindet),
Dazu musst Du dem Mesh wahrscheinlich ein Bounding Volume zuweisen. Weiß nicht, was Unity da benutzt, aber in den meisten Fällen ist es eine Bounding Sphere oder eine Axis Aligned Bounding Box (AABB).
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

Schrompf hat geschrieben: Dazu musst Du dem Mesh wahrscheinlich ein Bounding Volume zuweisen. Weiß nicht, was Unity da benutzt, aber in den meisten Fällen ist es eine Bounding Sphere oder eine Axis Aligned Bounding Box (AABB).
Stimmt das wars gewesen, nach etwas googlen hatte ich erst befürchtet ich müsse wie es einige gemacht haben Unitys Frustum Culling komplett austricksten mit einigen fiesen Workarounds.
Tatsaechlich wars aber nur eine Zeile Code, indem ich dem "Prototype"-Mesh was ich für das Rendern verwende (und dessen Vertices dann mit denen aus dem Compute Shader ersetze) ein passendes Bounding zuweise.
Jetzt bleibt der Planet wo er soll und ich kann mich endlich dem Verhalten nahe der Oberfläche zuwenden, d.h. Optimierung des LOD, Festlegen einer Grenze von Plane-Splits/Merges pro Frame, sowie ein Check ob sich Planes im Frustum befinden bevor ich splitte. Denn am Ende soll man auf dem Ding ja rumlaufen können 8-)

Bild
Benutzeravatar
xq
Establishment
Beiträge: 1581
Registriert: 07.10.2012, 14:56
Alter Benutzername: MasterQ32
Echter Name: Felix Queißner
Wohnort: Stuttgart & Region
Kontaktdaten:

Re: [Projekt] Prozedurales Universum

Beitrag von xq »

Sieht richtig cool aus mittlerweile! Ist das ein Waldplanet? Ich freu mich auf die ersten Ergebnisse von der Oberfläche :)
War mal MasterQ32, findet den Namen aber mittlerweile ziemlich albern…

Programmiert viel in ⚡️Zig⚡️ und nervt Leute damit.
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

MasterQ32 hat geschrieben:Sieht richtig cool aus mittlerweile! Ist das ein Waldplanet? Ich freu mich auf die ersten Ergebnisse von der Oberfläche :)
Danke dir. Ja perspektivisch soll das mal einer werden, so in grober Anlehnung an die Erde. Im Moment färbe ich erstmal nur bestimmte Terrainarten in einer bestimmten Farbe ein in Anlehnung an die jeweilige Art (braun/grün = Land, Grau = Berg, Weiß = Schnee), um ein Gefühl für die Oberfläche zu kriegen und zu schauen ob die Noise-Parameter einigermaßen passen. Die sind wie man sieht auch noch relativ einfach, was es noch nicht gibt sind bestimmte größere Biome, also Wüstenabschnitte oder Eis an den Polen.

Die Farben sollen dann später durch eine Textur ersetzt oder ergänzt werden. Und dann lassen sich im Prinzip auch gut Bäume drauf verteilen. Meine Idee wäre, wieder anhand von Noise, im Compute Shader Bereiche zu definieren wo Bäume gerendert werden sollen, und die dann spätzer beim Render der Planes ab einer bestimmten Quadtree-Tiefe mitzuzeichnen. Allerdings habe ich noch keine Idee, mangels Erfahrung mit Shadern, wo und wie ich am besten das Zeichnen von Bäumen etc. mache. Im Moment, das ist leider noch ein großes Problem, habe ich alle Noise- und Terraininformationen nur in der GPU. Letzten Endes müsste als das zeichnen und platzieren der Bäume auch irgendwo im Shader stattfinden.

Solange ich allerdings die Oberfläche noch nicht zum fliegen kriege wäre es zu früh sich darüber gedanken zu machen. Im Moment habe ich erstmal zwei ganz große Sorgen, nämlich
a) die Anzahl der Triangles in den Griff zu kriegen (noch habe ich leider keinen wirklich guten Frustum Culling Check im Quadtree in Unity implementieren können da lege ich meine größe Hoffnung rein) und
b) Habe ich heftigstes Z-Fighting wenn ich auf der Oberfläche bin (eklig, aber damit war leider fast zu rechnen :cry: ) dessen Elemenierung absolutes Neuland für mich ist.

Bild

Bild
Benutzeravatar
Schrompf
Moderator
Beiträge: 4838
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: [Projekt] Prozedurales Universum

Beitrag von Schrompf »

Schon sehr stylisch :-) Sieht für mich aber eher so aus, als wär auch Dein Boden halbtransparent.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

Ja sieht ziemlich druffig aus :-)

Ich hab den Eindruck die Reihenfolge in welcher die Planes gezeichnet werden passt nicht. Ggf. spielt die Reihenfolge eine Rolle in der welcher ich der GPU auffordere die Planes zu zeichnen.
Im Moment ist das ziemlich Fire-and-forget-mäßig. Ich hätte vermutet dass die Reihenfolge der DrawMesh Calls keine Rolle spielt weil ich dachte die GPU "weiß" immer welche Triangles vor den anderen sind, aber ggf. ist die Denke der Fehler? Falls ja müsste ich die Quadtrees im der Render Queue nochmal nach Distanz sortieren. Falls das das Problem wäre.

Code: Alles auswählen

    /// <summary>
    /// Called for rendering the planet each frame. It sets the material and calls the shader to draw all elements in the QuadtreeTerrainRenderQueue
    /// </summary>
    void OnRenderObject()
    {
        for (int i = 0; i < this.QuadtreeTerrainRenderQueue.Count; i++)
        {
            QuadtreeTerrain quadtreeTerrain = (QuadtreeTerrain)this.QuadtreeTerrainRenderQueue[i];
            if (quadtreeTerrain.quadtreeTerrainState == QuadtreeTerrain.QuadtreeTerrainState.READY)
            {
                quadtreeTerrain.material.SetBuffer("patchGeneratedFinalDataBuffer", quadtreeTerrain.patchGeneratedFinalDataBuffer);
                Graphics.DrawMesh(this.prototypeMesh, transform.localToWorldMatrix, quadtreeTerrain.material, LayerMask.NameToLayer(GlobalVariablesManager.Instance.layerLocalSpaceName), null, 0, null, true, true);
            }
        }
    }
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: [Projekt] Prozedurales Universum

Beitrag von Krishty »

Von sich aus weiß die GPU da nichts, und zeichnet einfach in der Reihenfolge, in der die Daten eintreffen. Gut möglich, dass Unity Abhilfe anbietet (ist ja ein weit verbreitetes Problem), aber sortiert werden muss auf jeden Fall – wenn nicht von Unity, dann eben von dir.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

Hoffe daran liegts, dann wäre das Problem ja recht einfach lösbar. Ich probiers aus und sortiere die Render-Queue nochmal vor dem Zeichnen. Update folgt.

EDIT: Ich habe die Sortierung jetzt ergänzt. Hat die Situation nicht verbessert. Allerdings habe ich nun mal mit den Quality Settings gespielt und von "Good" auf "Fantastic" gestellt (hätte ich viel früher machen sollen),
jetzt erkennt man auch etwas besser was passiert. Es scheint so zu sein dass die Lichtreflektionen durch Objekte im Vordergrund hindurchgehen, und man daher Konturen von Objekten erkennen kann.
Ich habe das mal gefilmt, da erkennt man gut was passiert. Die Frage ist wie man dem Abhilfe verschaffen kann.

[youtube]2eREUGvSUiA[/youtube]

EDIT 2: Habe den Fehler gefunden. Für jeden der mal in das gleiche Problem läuft: Man muss beim Verwenden einer Lightsource darauf achten dass Shadowing enabled ist. Ich hätte nicht erwartet dass deaktiviertes Shadowing sofort dazu führt dass ein Licht jedes Hindernis "ignoriert". Aber gut. Wenn man das tut führt das bei einem Point Light allerdings schnell zu Problemen (Flackern, notNormalized(normal)-Exception, etc) wenn man es zu weit weg vom Vector(0,0,0) Origin platziert - was beim beleuchten einer Sphäre mit Erdradius aber notwendig ist. Daher ist das ein guter Moment spätestens jetzt zu einer "Directional Lighntning"-Source zu wechseln.
Was der Beleuchtung auf der Erde aber ggf. gar nicht so unähnlich ist. Die Strahlen der Sonne dürften aufgrund der Extremen Distanz auch genähert parallel auf die Erde treffen.

Problem gelöst, als nächstes ist jetzt nur noch kritisch Culling und Optimierungen umzusetzen für mehr Performance (und mehr Oberflächen-Details). Habe ein neues Video aufgenommen. Dabei zwar noch einen weiteren Bug entdeckt (der Planet merged nicht richtig auf geringes LOD zurück, man siehts an der Framerate im Video :lol: ).
Aber ich war von der Schönheit so beeindruckt dass ich weiter aufnehmen und einen separaten Screenshot machen musste :mrgreen:

[youtube]NeUOW3jYapo[/youtube]

Bild
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: [Projekt] Prozedurales Universum

Beitrag von Krishty »

Gratuliere :)
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

Ich werde jetzt mal probieren eine Normalmap im Shader zu generieren. Ich glaube per-Vertex-Normals bringen mich nicht weiter, man ist zu stark dazu verleitet zu splitten um mehr Terraindetail zu erhalten, da komme ich unweigerlich in Probleme mit der Anzahl an Triangles. Und das gewonnene Wissen hilft vielleicht auch dabei wenn es später ums Texturieren im Shader geht.

Im ersten Schritt versuche ich jetzt erstmal eine RWTexture2D in der gleichen Auflösung wie das Mesh im Compute Shader mit einfachen Sphere-Normalen zu befüllen und dann zum Render-Shader (im Surface-Shader?) zur Anwendung zu kriegen.
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

Ich habe heute mal angefangen mit NormalMapping zu experimentieren. Mein Ziel ist die Normalen nicht im Surface Shader zu berechnen etwa auf Basis der Noise Werte (da dies dann in jedem Frame geschehen würde),
ebenso will ich die Textur nicht auf der CPU aufbauen (da ich dann von der GPU zur CPU flushen müsste, was ebenfalls wieder Zeit kostet).
Seit Unity 5 ist Render-To-Texture auch in der Personal Edition möglich, also genau das was ich brauche um die NormalMap auf der GPU im Compute Shader einmal zu erzeugen und dann im Surface Shader zu verwenden.

Ziel ist dann natuerlich mehr Flexibität zu haben wenn es zur Erzeugung von Terrain Details geht und gleichzeitig die Anzahl der Triangles gering zu halten (Performance-Seitig neben dem nicht funktionierenden Frustum Culling meine größte Schwierigkeit). Zweiter Schritt wäre dann natürlich auch echte Texturen (Grass, Felsen etc., auf Basis einer AtlasTextur) im Shader zu erzeugen (dann vermutlich im Surface Shader?) und nicht mehr nur mit Vertice-Farben zu arbeiten. Aber das kommt später, erstmal die NormalMap. Außerdem lerne ich so mit Texturen in Shadern umzugehen.

Deswegen habe ich heute mal versucht die Grundlagen zum Fliegen zu kriegen, d.h. eine RenderTextur in einem Compute Shader zu befülllen (erstmal in der gleichen Auflösung die die Vertex-Dichte) und dann im Surface Shader anzuwenden. Um zu sehen was passiert habe ich erstmal die _MainTex mit dem Output belegt und o.color verändert. Die gute Nachricht ist, es scheint grundsätzlich zu funktionieren, die Ergebnisse sind sagen wir zielversprechend dass ich nicht völlig daneben liege :lol:

Compute Shader:

Code: Alles auswählen

float w = 32
float h = 32;
float3 normalRGB = float3(id.x/w,id.y/h,1);
patchGeneratedNormalMapTexture[id] = float4(normalRGB,1);
Vertex Shader:

Code: Alles auswählen

o.uv_MainTex = v.texcoord.xy;
Surface Shader:

Code: Alles auswählen

fixed3 crgb = tex2D(MainTex, IN.uvMainTex).rgb;
fixed4 c = float4(crgb, 1);
o.Albedo = clamp(c.rgb, fixed3(0, 0, 0), fixed3(1, 1, 1));
--> führt zu
Bild

Anpassung des Compute Shader nach:

Code: Alles auswählen

float3 normalRGB = float3(1,0,0);
[i]oder[/i]
float3 normalRGB = float3(0,1,0);
[i]oder[/i]
float3 normalRGB = float3(0,0,1);
patchGeneratedNormalMapTexture[id] = float4(normalRGB,1);
--> führt zu
Bild

Bild

Bild

Anpassung des Compute Shader nach:

Code: Alles auswählen

float3 normalRGB = float3(noise,noise,noise);
patchGeneratedNormalMapTexture[id] = float4(normalRGB,1);
--> führt zu
Bild

Zuguterletzt Anpassung des Compute Shader nach:

Code: Alles auswählen

float3 normalRGB = normal.xyz;
patchGeneratedNormalMapTexture[id] = float4(normalRGB,1);
--> führt zu
Bild

Das heisst die Textur-Informationen kommen an wie erwartet :D .
Nächster Schritt ist die NormalMap so zu encoden dass sie im Surface Shader richtig auslesbar ist. Das ist teilweise verwirrend, im Internet sieht man dass teilweise die normalen beim Schreiben der Textur nochmal geteilt und geclampt weren und sie in den Wertebereich 0 bis 1 zu bringen, und das gleiche nochmal im Surface Shader umgekehrt ausgeführt wird. Ebenso wird teils im Surface Shader beim Auslesen noch die Funktion "UnpackNorm" verwendet, was immer die ich auch tut. Das alles führte teils zu schrägen Ergebnissen bzw. es war kein richtiger Effekt von Normalen mehr erkennbar, bis ichs mal mit der denkbar einfachsten Variante probiert habe.

Compute Shader:

Code: Alles auswählen

float3 normalRGB = normal.xyz;
patchGeneratedNormalMapTexture[id] = float4(normalRGB,1);
Vertex Shader:

Code: Alles auswählen

o.uv_NormalMap = v.texcoord.xy;
Surface Shader:

Code: Alles auswählen

fixed3 normal = tex2D(NormalMap, IN.uvNormalMap);
o.Normal = normal;
Bild

Jetzt ist gefühlt ein Einfluss von NormalMapping erkennbar, gar nicht schlecht. Was ich jetzt wohl als erstes in den Griff kriegen muss sind die richtigen Texturkoordinaten.
Vielleicht ist danach schon alles wie es sein soll.

Oder es hängt damit zusammen dass ich nochmal eine Art Worldspace Transformation machen muss, ich habe ein paar Beispiele gesehen wo manche da noch was gemacht haben im Shader. Allerdings spricht der Screenshot dagegen wo man sieht wie ich die Noise-Informationen als _MainTex verwende, das sieht dann eher nach einem Koordinaten Problem aus wenn ich nicht falsch liege. HIer auch nochmal das NormalMapping auf grauer Oberfläche.
Dürften Texturkoordinaten sein, was denkt ihr?

Bild

EDIT: Hab den Fehler gefunden. Bei ganz genauem Hinsehen fiel mir auf dass die Normalmap scheinbar jeweils um 90 Grad gedreht ist.
Eine Anpassung im ComputeShader, indem ich die X und Y Koordinaten vertausche, hat die Lösung gebracht. Ich muss nochmal drüber nachdenken warum das so ist, aber jetzt funktioniert das Normalmapping.

Compute Shader

Code: Alles auswählen

uint2 normalMapID = uint2(id.y,id.x);
patchGeneratedNormalMapTexture[normalMapID] = float4(normalRGB,1);
Bild

Bild

Zeit jetzt als nächstes mit hochaufgelösten Normalmaps zu arbeiten um die Früchte von Normalmaps zu ernten :lol: :D 8-) (hier muss ich jetzt vermutlich meine Noise/Normalen-Implementierung im Compute Shader anpassen weil ich jetzt nicht mehr nur pro Vertex einen Noise Call brauche sondern haeufiger pro Texturpixel).
Benutzeravatar
xq
Establishment
Beiträge: 1581
Registriert: 07.10.2012, 14:56
Alter Benutzername: MasterQ32
Echter Name: Felix Queißner
Wohnort: Stuttgart & Region
Kontaktdaten:

Re: [Projekt] Prozedurales Universum

Beitrag von xq »

Sehr cool, das wird auf jeden Fall was! Mit Shading sieht auch alles immer gleich viel besser aus...
War mal MasterQ32, findet den Namen aber mittlerweile ziemlich albern…

Programmiert viel in ⚡️Zig⚡️ und nervt Leute damit.
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

So, die Shader sind jetzt umgebaut. Weg von Per-Vertex-Normals, hin zu NormalMaps. Weiterhin generiere ich jetzt auch eine SurfaceTexture in der gleichen Auflösung wie die NormalMap.
Heisst jetzt zwei ComputeShader Stages um eine 128x128 NormalMap und 128x128 SurfaceTexture zu erstellen, sowie ein dritter um aus den Daten noch einen 32x32 ComputeBuffer mit Vertex-Positions zu generieren.
Der Umstand dass der Terrain-Detailgrad jetzt nicht mehr von der Anzahl der Vertices abhängt hat geholfen das Terrain jetzt schon eine ganze Ecke detailierter zu machen, bei gleichzeitig geringer Anzahl Triangles.
Letztere konnte ich nochmal drücken indem ich jetzt auch weitestgehend funktionierendes Frustum Culling habe, indem ich für jede Plane jetzt separate bounds setze vor dem Rendern. Weitestgehend, weil nahe der Oberfläche noch zu früh geculled wird, da muss ich noch rausfinden warum.

Anyway, es gibt Progress...

Bild

Bild

Bild

Bild

Problem ist aktuell noch ein Performance-Einbruch sobald ich näher an die Oberfläche komme und so ca. auf Tiefe 9-10 meiner Quadtrees bin. Da geht die Performance ganz deutlich noch in den Keller, man sieht in den Stats dass die CPU Zeit irgendwann in dem Bereich auf ca. 50-100ms pro Frame hochgeht. Jetzt heisst es Profiling um rauszufinden woran dass liegt. D.h. Messen ob die Zeit im rekursiven Scan der 6 Quadtrees verloren geht (im Moment mache ich noch einen rekursiven Fullscan aller 6 Quadtrees pro Frame) bzw. bei einer bestimmten Aktion (z.B. die Cullchecks), oder eher beim Split/Merge der Quadtree Nodes in der Queue (ebenfalls alle pro Frame), oder der Umstand dass ich zu viel zur GPU Dispatche. Mal schauen....
Benutzeravatar
guldenguenter
Beiträge: 20
Registriert: 10.12.2015, 23:06

Re: [Projekt] Prozedurales Universum

Beitrag von guldenguenter »

Finde das Projekt, vor Allem für mich als SciFi Fan, sehr sehr spannend und die Bilder/Videos sehen echt gut aus!
Da deine Videos so aussehen, als ob du diese vom Bildschirm abgefilmt hast, kann ich dir OBS (Open Broadcaster Software) empfehlen. Ist open source und mit paar Klicks hast sehr einfach ein Fenster recorded!
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

guldenguenter hat geschrieben:Finde das Projekt, vor Allem für mich als SciFi Fan, sehr sehr spannend und die Bilder/Videos sehen echt gut aus!
Da deine Videos so aussehen, als ob du diese vom Bildschirm abgefilmt hast, kann ich dir OBS (Open Broadcaster Software) empfehlen. Ist open source und mit paar Klicks hast sehr einfach ein Fenster recorded!
Jepp da bin ich mittlerweile drauf umgestiegen. Die ersten Videos hatte ich in der Tat noch vom Bildschirm abgefilmt :oops: , mittlerweile nutze ich auch OBS. Echt ein sehr sehr feines Tool.

Bei meinem Culling Problem ahne ich worans liegt, da gibts leichte Verbesserung, ganz gelöst ist das Problem aber noch nicht.
Ansonsten schaue ich gerade wie ich die Performance etwas verbessern kann. Vorher musste ich bei jedem Frameupdate nochmal manuell die Bounds pro Plane neu setzen, das passiert jetzt nur noch einmal bei der Initialisierung einer neuen Plane. Dann habe ich noch das Parsen der Quadtrees in einen seperaten Thread verlegt. Der Hauptthread tut jetzt pro Frame nur noch schauen ob in der Queues Sachen sind um sie zum Compute Shader zu geben, sowie dann noch die Draw-Calls aller Planes zum Vertex/Surface Shader.
Im Moment experimentiere ich mit den Mesh und Texture Größen sowie dem LOD. Deswegen stotterts in dem Video hier und da auch.

Allerdings merke ich jetzt das mich das Precision Problem wohl ein erneutes Mal erwischt hat. Sphere Radius ist 6371000, Plane Vertice-Auflösung ist 32x32, SurfaceTexture und Normalmap 128x128, maximale quadtree node Tiefe ist 18, maximale Anzahl Noise Octaven ist 23.

[youtube]G8Jk_Y4wlwo[/youtube]

Beim höchten Nodelevel (18) treten jetzt horizontale und vertikale Linien auf, und es sind Fehler in der Noise-Berechnung erkennbar. Darüberhinaus erkennt man nun positional jitter.
Schätze die Texturen-Probleme resultieren aus Float-Precision Problemen im Compute Shader, wo die Texturen berechnet werden. Das positional jitter könnte entweder daher kommen dass für jede Plane der Sphere-Mittelpunkt als relativer Referenzpunkt für die Positionierung genommen wird - der Planet besitzt nur GameObject mit der Center Koordinate in der Mitte. D.h. in dem Moment in dem ich zum Beispiel direkt vor einer Plane bin ist die Sphere Center Koordinate bei Position (0,0,6371000). Entweder ist es dass, oder wieder ein Precision Problem im Shader, diesmal im Vertex Shader, wo die Plane auf ihre finale Koordinate gebracht wird (dort wird die Plane von 0,0,0 auf die World Space Center Koordinate des node verschoben).

Ich wünschte mir wirklich wir wären irgendwann soweit dass Engines wie Unity double precision anbieten würden, und gleiches irgendwann auch bei Shadern passieren würde.
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: [Projekt] Prozedurales Universum

Beitrag von Krishty »

Quasi jede GPU hat heutzutage double-Support. Bloß ist die Latenz übel (oft vier Mal so langsam wie float) und man kann in Texturen weiterhin bloß float speichern (alles andere wäre aber auch Irrsinn).
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

Nicht viel neues, aber mal wieder ein kleines Update.
Im Moment mache ich viel Code-Cleanup sowie einige Experimente mit den aktuellen Unity3D Betas. Im Zuge des Code-Cleanup bin ich dabei jetzt die wichtigsten Routinen auszulagern und zu parameterisieren, um sie für verschiedene Objekte, nicht nur Planeten, zu verwenden (d.h. zum Beispiel der ganze Quadtree Parser Code, ComputeShader etc.). Testweise bin ich nun herangegangen die Asteroiden, die vorher auf der CPU erzeugt wurden, jetzt auch auf der GPU zu machen. Klappt soweit ganz gut, unten ein Beispiel mit 10 KM durchmesser und im Bild mittleren LOD. Schwierigkeiten hatte mir der gleichzeitige Wechsel von Unity 5.2 auf 5.3 gemacht mit dem Umstand dass zwischen den Versionen offensichtlich einige Default Einstellungen geändert wurden (z.B. Format der RenderTexture).

Aber in dem Zuge habe ich auch gleichzeitig etwas über das Shaderverhalten in Unity gelernt (Stichwort VertexShader-Normale und FragmentShader-Normale, und was passiert wenn man die kombiniert). Egal, das ganze tut jetzt erstmal unter Unity 5.3. Soweit so gut.

Bild

Ich werde unter Unity 5.3 jetzt nur noch bedingt weiterentwickeln, auch was das Thema Präzision und das jittering angeht, und beobachte erstmal die nächsten Betas und Major-Versionen von Unity.
Hintergrund ist der Umstand dass ich intensiv ComputeBuffer aus den ComputeShadern an die Vertex/Fragment Shader weiterreiche, das verhindert aktuell das Batching zwischen CPU und GPU. Aktuell funktioniert Batching nur mit Vector, float und Texture Typen. Das drückt die Performance in meinem Fall extremst.
Im Unity Beta Forum habe ich aber jetzt von den Unity Entwicklen das Feedback erhalten dass sie in die entsprechenden Klassen jetzt auch eine SetBuffer() Funktion einbauen wollen - wenn das passiert dann dürften alle Objekte die ich aktuell zeichne in der Variante wie ich das umgesetzt habe in nur einem einzigen Batch gerendert werden. Ich hoffe dass das klappt, wenn ja dann dürfte sich das extremst positiv auf die Performance auswirken, wenn nicht komme ich langsam in einer Sackgasse unter Unity an.

Aktuell überlege ich wie ich später eine größere Anzahl Asteroiden instanzieren will, da mein Ziel ist eine möglichst große Menge in einer Szene zu zeichnen. Kernfrage ist ob wirklich jeder Asteroid individuell sein soll oder ob ich ggf.
damit arbeite nur Gruppen von unterschiedlichen Asteroiden zu machen, dann könnte ich unter Unity vermutlich mit GPU Instancing arbeiten. Abr sinnvoll damit experiementieren sollte ich vermutlich erst wenn ich weiß welche Auswirkung die Ergänzung der Buffers für das Batching hat.
smurfer
Establishment
Beiträge: 195
Registriert: 25.02.2002, 14:55

Re: [Projekt] Prozedurales Universum

Beitrag von smurfer »

Hi sushbone,

fachlich habe ich gar nicht viel beizutragen, finde es aber ein schönes Projekt und freue mich immer, Neues zu lesen. So ich denn Zeit finde, bin ich -- wenn auch nur in 2D -- in ähnlicher Richtung, sprich prozedural generiertes Universum, unterwegs (wobei der Fokus eher auf der Physik liegt). Daher wahrscheinlich das besondere Interesse.

Wenn ich mich recht entsinne, hat Josh Parnell mit seinem Kickstarter-Projekt Limit Theory einen ähnlichen Weg bei Astroiden eingeschlagen: Einige Prototypen, die dann unterschiedlich skaliert oder nachbearbeitet werden. Die genauen Details kenne ich leider nicht mehr, sollte aber noch in den Unmengen seiner urpsprünglich täglichen Dev Logs stehen. Ähnlichkeiten aufgrund der limitierten Zahl an Astroidenprototypen sind mir in den Screenshots und auch im damalig spielbaren Prototypen nicht aufgefallen.

Beste Grüße
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

Nachdem nun die neue Version von Unity3D erschienen ist, Version 5.4, die auch einige neuerungen wie unter anderem GPU Instancing mitgebracht hat, wird es mal wieder Zeit an dem Programm weiterzumachen.

In dem Rahmen strukturiere ich auch nun nochmals die Anwendung etwas um, um hier und da noch Performance-Optimierungen rausholen zu können. Auch will ich das platzieren von Objekten neu machen. Bis dato habe ich Sonnen und Planeten alle auf Basis von Simplex Noise gleichverteilt, das soll sich jetzt ändern, sodaß jetzt Planeten primär nur noch um Sonnen (bzw. Massereiche Objekte) platziert werden. Sowie es dass es jetzt auch soetwas wie Galaxie-Formationen gibt. Bei letzterem bin ich aber noch auf der Suche nach einem Algorithmus der mir erlaubt solche "strudelartigen" Formationen nachzubilden. Naja, und in dem Rahmen fang ich jetzt auch mal an rudimentäre Menüs und ähnliches zu ergänzen.

Erster Schritt ist jetzt das GPU Instancing einmal anzuwenden, wie oben geschrieben testweise mit Asteroiden die sich dazu gut eignen sollen. D.h. erzeugen von ca. 100 prozedural generierten Asteroiden die dann wenn benötigt instanziiert werden. Hier schaue ich gerade wie sich das in Kombination mit LOD in Unity am besten machen lässt.

Parallel bin ich mit einem weiteren Forum-Member aus dem I-Novae Forum noch dran das Planeten-Rendern zu verbessern. Ein wichtiger Punkt ist dabei weiterhin die Draw-Calls für die Planeten Planes effektiver hinzukriegen.
Größtes Problem ist dabei nach wie vor dass die separaten Materialien (NormalTexture etc.) pro Plane das Batching verhindern. Bedauerlicherweise hat sich heraus gestellt dass die SetBuffer() Ergänzung in die MaterialBlockProperties keine Hilfe für das Batching darstellt - sobald sich eine Buffer- oder Textur-Referenz ändert ists vorbei mit dem Batching :cry: . Nun gut...

Nach Kontakt mit einem Unity-Entwickler in deren Forum scheint der aktuell einzige Ausweg zu sein, für die Planes gesharte Texturen und ComputeBuffer zu verwenden. Finde ich nicht ideal, will ich mir aber zumindest einmal ansehen. Während das bei ComputeBuffern relativ einfach zu bewerkstelligen ist bin ich im ersten Schritt an den Texturen dran. Ein Weg wäre ggf. Texture2DArrays zu verwenden, ich wills aber erstmal mit einer Atlas-Variante probieren wie er auch vorgeschlagen hatte. Da arbeite ich also gerade an einer Art dynamischem Pooling und Textur-Management, sodaß neue Planes on demand eine neue oder bereits vorhandene Textur bekommen, die sie dann mithilfe von separaten UV Koordinaten nutzen können.

Eine Frage an der ich vorbei kam und bei der ihr mir ggf. helfen könnnt: Macht es für die Grafikkarte prinzipiell einen Unterschied, wenn ich dort einen gleichmäßigen (Width und Height) hinschicke vs. einen der zum Beispiel nur in der Height größer ist? Angenommen ich habe 32x32 Texturen pro Plane und möchte vier Planes in einer gesharten Textur abbilden, so könnte ich entweder eine 64x64 Textur zur GPU schicken, oder aber auch eine 32x128 Textur. Ist das für die Grafikkarte in irgendeiner Form relevant?
Benutzeravatar
Schrompf
Moderator
Beiträge: 4838
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: [Projekt] Prozedurales Universum

Beitrag von Schrompf »

Die Frage kann ich beantworten: nein. Du hast nach letztem Standard bis 8192 auf jeder Kante Platz, die Gesamtgröße im Speicher ist aber zusätzlich begrenzt auf 128MB oder so. Muss aber nicht quadratisch sein, und genau genommen muss es auch keine Zweierpotenz mehr sein. Das war früher mal notwendig, aber schon zu DX9-Zeiten haben alle ernstzunehmenden Grafikkarten auch ohne leben können.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: [Projekt] Prozedurales Universum

Beitrag von Krishty »

... sofern du nicht die Mindestgröße des GPU-internen Tilings unterschreitest (32x32 Texel oder so). 4096x1 wäre also z.B. wieder nicht so pralle.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

Die Frage kann ich beantworten: nein. Du hast nach letztem Standard bis 8192 auf jeder Kante Platz, die Gesamtgröße im Speicher ist aber zusätzlich begrenzt auf 128MB oder so. Muss aber nicht quadratisch sein, und genau genommen muss es auch keine Zweierpotenz mehr sein. Das war früher mal notwendig, aber schon zu DX9-Zeiten haben alle ernstzunehmenden Grafikkarten auch ohne leben können.
Danke, das klingt doch erstmal gut! I mags gerne nach dem Prinzip "keep it simple", und in meinem Problemfall sehe ich keinen Grund ein Quadrat aufzufächern. Im Gegenteil, wenn ich die verschiedenen Texturabschnitte untereinander organisiere kann ich leichter die Texturen zur Laufzeit um weitere einzelne Texturabschnitte erweitern oder verkürzen, und schrittweise um einen weiteren Abschnitt erweitern, ohne sofort eine ganze Zweierpotenz anlegen und reservieren zu müssen.
... sofern du nicht die Mindestgröße des GPU-internen Tilings unterschreitest (32x32 Texel oder so). 4096x1 wäre also z.B. wieder nicht so pralle.
Alles klar. Nein das sollte denke ich nicht passieren. Momentaner Gedanke ist die Textur pro Plane so im Bereich 128x128 bis 256x256 vorzusehen, das hat in meinem letzten Prototyp ganz gut geklappt und war ein guter Kompromiss zwischen Detail und Performance. Heisst wenn die shared Texturen dann mehrere dieser Tiles aufnehmen müssten wären die irgendwo in den Größenordnungen von je nachdem:
128x256 (zwei Plane-Texturen)
128x384 (drei Plane-Texturen)
128x512 (vier Plane-Texturen)
128x640 (fünf Plane-Texturen)
128x768 (sechs Plane-Texturen)
128x896 (sieben Plane-Texturen)
128x1024 (acht Plane-Texturen)
....
bzw.
256x512 (zwei Plane-Texturen)
256x768 (drei Plane-Texturen)
256x1024 (vier Plane-Texturen)
256x1280 (fünf Plane-Texturen)
256x1536 (sechs Plane-Texturen)
256x2792 (sieben Plane-Texturen)
256x2048 (acht Plane-Texturen)
....

Wo ich dann lande hängt davon ab wieviel Performance ich durch ein effizienteres Batching aufgrund der shared Texturen rausholen kann. Bin gespannt ob das ganze fliegt...
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

So die Service-Klassen für shared Texturen sind fertig, an sich super simpel und unspektakulär alles.
SharedTexture is eine Klasse/Instanz welche ein Texture2D Objekt enthält in der Dimension Width*Slots, also z.B. 256x2048. Daneben noch ein Array mit bools die angeben ob der jeweilige Bereich der Texture genutzt wird. Die Länge des Array gibt gleichzeitig die Anzahl der vergübaren Slots an.
SharedTextureManager beinhaltet eine Liste von SharedTextures und steuert die Instanziierung neuer SharedTextures wenn benötigt. Gleichzeitig entfernt er sie auch wieder. Er selbst wird instanziiert mit einer Width Angabe sowie einer Slots Angabe, d.h. alle SharedTextures haben die gleiche Width und Anzahl nutzbarer Slots (theoretisch spricht aber nichts dagegen die Anzahl von Slots zur Laufzeit zu ändern für neu instanziierte SharedTextures). Im Außenkontext (also andere Klassen die einen Slot einer SharedTexture nutzen wollen) instanziiert er "SharedTextureSlot" Instanzen aus der entsprechenden Klasse.
Wird eine SharedTextureSlot Instanz angefordert, so geht er durch die Liste von SharedTextures durch und sucht in dem jeweiligen bool-Array nach einem freien Slot. Findet er einen, so packt er die SharedTexture mit der Angabe des freien Slot (der dann in dem bool-Array gesperrt wird) und einer Referenz auf sich selbst (SharedTextureManager) in die SharedTextureSlot Instanz und gibt diese nach außen.
SharedTextureSlot wird dann im äußeren Kontext genutzt. Die Klasse bietet einen Getter auf die Textur, die UVKoordinate des Slot sowie eine Methode Release(). Release ruft über die Referenz des SharedTextureManager eine dortige funktion auf (und übergibt SharedTextureSlot). Die Funktion gibt den Slot im bool-Array wieder frei. Außerdem prüft sie ob ggf. alle bool Variablen darauf hindeuten dass die gesharete Textur ungenutzt ist, falls ja wird sie entfernt und die Instanz von SharedTexture aus der Liste genommen. Darüber wird quasi bei jedem Release eines Slots gleichzeitig das Housekeeping betrieben.

SharedTexture

Code: Alles auswählen

using UnityEngine;
using System.Collections.Generic;

/// <summary>
/// Author: Joerg Zdarsky | joerg.zdarsky@gmx.de
/// Date: 2016-08-05
/// SharedTexture includes a Texture2D object that is meant to be used across different slots. Thus, it is larger in one dimension as the textures used "in the outer context".
/// The first segment of the texture is meant to have index 0.
/// The Width of the texture is always the same (applied in the constructor), while the height extends with the number of indexes.
/// E.g. a texture with width=64 can be 64x64 (slots=1) or 64x128 (slots=2) or 64x196 (slots=3) and so on. Its dimension only increases at the height.
/// </summary>
public class SharedTexture : MonoBehaviour {

    public string UUID;
    public RenderTexture texture;       // The core texture that is shared across
    public bool[] isUsable;             // Array of bools, where each bool marks if a slot is usable
    private int singleWidth;            // Width of single texture area among the shared texture
    private int slotsWidth;             // Number of texture slots in width direction
    private int slotsHeight;            // Number of texture slots in height direction

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="singleWidth">Width of texture</param>
    /// <param name="slotsHeight">Maximum number of texture slots among the shared texture. 1 slot means that the texture is only used once while 2 slots means a shared texture is used twice.</param>
    public SharedTexture(int singleWidth, int slotsWidth, int slotsHeight, RenderTextureFormat renderTextureFormat)
    {
        // Check parameters
        if (singleWidth * slotsWidth > 8192)
            Debug.LogWarning("SharedTexture::With the provided values of single texture width and slots your texture width exceeds 8192 pixels which is not supported on most graphic cards.");
        if (singleWidth * slotsHeight > 8192)
            Debug.LogWarning("SharedTexture::With the provided values of single texture width and slots your texture height exceeds 8192 pixels which is not supported on most graphic cards.");
        if (singleWidth > 8192)
            Debug.LogWarning("SharedTexture::Your width exceeds 8192 pixels which is not supported on most graphic cards.");
        if (singleWidth < 32)
            Debug.LogWarning("SharedTexture::Your width is below 32 pixels which is not recommended for efficiency reasons.");
        if (singleWidth < 1)
            Debug.LogError("SharedTexture::You width is below 1 pixel.");

        // Set the core variables
        this.UUID = new System.Guid().ToString();
        this.singleWidth = singleWidth;
        this.slotsWidth = slotsWidth;
        this.slotsHeight = slotsHeight;

        // Initialize texture
        this.texture = new RenderTexture(singleWidth * slotsWidth, singleWidth * slotsHeight, 0, renderTextureFormat);
        this.texture.enableRandomWrite = true;
        this.texture.Create();

        // Initialize Slot Indicator array
        this.isUsable = new bool[slotsWidth * slotsHeight];
        for (int i = 0; i < isUsable.Length; i++)
            this.isUsable[i] = true;
        Debug.Log("SharedTexture::Initialized with constructor. Overall texture dimension=" + texture.width+"x"+texture.height+". Slots="+this.isUsable.Length);
    }

    /// <summary>
    /// Returns if the texture is being used in one of its slots.
    /// </summary>
    /// <returns>true if one of the texture slots of the shared texture is being used, false if none of the texture slots are being used.</returns>
    public bool isTextureUsed()
    {
        bool result = false;
        for (int i = 0; i < isUsable.Length; i++)
            if (this.isUsable[i] == false) return true;
        return result;
    }

    /// <summary>
    /// Returns if the texture slot is usable (free and not out of array).
    /// </summary>
    /// <param name="slot">index of the slot (0 to n)</param>
    /// <returns>returns if the texture slot at the provided index is usable</returns>
    public bool isSlotUsable(int slot)
    {
        if (slot < 0 || slot > isUsable.Length - 1)
            Debug.LogError("SharedTexture::An isSlotUsable() was requested with the provided slot index being out of bounds. array=0 to " + (this.isUsable.Length - 1) + ", slot=" + slot);
        if (slot < 0 || slot > isUsable.Length - 1) return false;
        else return this.isUsable[slot];
    }

    /// <summary>
    /// Returns the PixelOffset vector of the requested index.
    /// </summary>
    /// <param name="slot">index of the slot (0 to n)</param>
    /// <returns>UVOffset Vector2</returns>
    public Vector2 PixelOffset(int slot)
    {
        if (slot < 0 || slot > isUsable.Length - 1)
            Debug.LogError("SharedTexture::PixelOffset(...) was requested with the provided slot index being out of bounds. array=0 to " + (this.isUsable.Length-1) + ", slot=" + slot);
        // Convert into 2d-index
        int x = slot % this.slotsWidth; 
        int y = slot / this.slotsWidth;
        Vector2 result = new Vector2(this.singleWidth * x, this.singleWidth * y);
        return result;
    }

    /// <summary>
    /// Returns the UVOffset vector of the requested index.
    /// </summary>
    /// <param name="slot">index of the slot (0 to n)</param>
    /// <returns>UVOffset Vector2</returns>
    public Vector2 UVOffset(int slot)
    {
        if (slot < 0 || slot > isUsable.Length - 1)
            Debug.LogError("SharedTexture::UVOffset(...) was requested with the provided slot index being out of bounds. array=0 to " + (this.isUsable.Length - 1) + ", slot=" + slot);
        // Convert into 2d-index
        int x = slot % this.slotsWidth;
        int y = slot / this.slotsWidth;
        Vector2 pixelOffset = this.PixelOffset(slot);
        float UVx = pixelOffset.x / (float)this.Width();
        float UVy = pixelOffset.y / (float)this.Height();
        Vector2 result = new Vector2(UVx, UVy);
        return result;
    }

    /// <summary>
    /// Returns the scaling and offset in a vector4
    /// </summary>
    /// <returns>x contains X tiling dimension value, y contains Y tiling dimension value, z contains X offset value, w contains Y offset value.
    /// Tiling dimension is the amount a slot is smaller than the overall texture dimension in width and height direction.</returns>
    public Vector4 Offset(int slot)
    {
        if (slot < 0 || slot > isUsable.Length - 1)
            Debug.LogError("SharedTexture::Offset(...) was requested with the provided slot index being out of bounds. array=0 to " + (this.isUsable.Length - 1) + ", slot=" + slot);
        Vector2 UVOffset = this.UVOffset(slot);
        Vector2 Scaling = new Vector2(1.0f / (float)this.slotsWidth, 1.0f / (float)this.slotsHeight);
        Vector4 result = new Vector4(Scaling.x, Scaling.y, UVOffset.x, UVOffset.y);
        return result;
    }

    /// <summary>
    /// Returns texture width
    /// </summary>
    /// <returns></returns>
    public int Width()
    {
        return texture.width;
    }

    /// <summary>
    /// Returns texture height
    /// </summary>
    /// <returns></returns>
    public int Height()
    {
        return texture.height;
    }

    /// <summary>
    /// Returns number of slots
    /// </summary>
    /// <returns></returns>
    public int Slots()
    {
        return this.isUsable.Length;
    }
}
SharedTextureManager

Code: Alles auswählen

using UnityEngine;
using System.Collections.Generic;

/// <summary>
/// Author: Joerg Zdarsky | joerg.zdarsky@gmx.de
/// Date: 2016-08-05
/// SharedTextureManager manages the list of SharedTexture instances, and instanciates SharedTextureSlot instances.
/// It is taking care if a free slot is available in any of the SharedTexture instances, and does housekeeping if a SharedTextureSlot is being released.
/// </summary>
public class SharedTextureManager {

    public List<SharedTexture> sharedTextureList = new List<SharedTexture>();   // List of sharedTextures
    public int width;                                                           // Width of each SharedTexture. It also represents the height of a single texture slot.
    public int slotsWidth;                                                      // Number of slots of each SharedTexture in width direction
    public int slotsHeight;                                                     // Number of slots of each SharedTexture in height direction
    private RenderTextureFormat renderTextureFormat;                            // Type of RenderTextureFormat the shared textures are of
    private object type;

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="slotsHeight">maximum slots per texture</param>
    /// <param name="width">width of the texture</param>
    public SharedTextureManager(int width, int slotsWidth, int slotsHeight, RenderTextureFormat renderTextureFormat)
    {
        this.sharedTextureList = new List<SharedTexture>();
        this.width = width;
        this.slotsWidth = slotsWidth;
        this.slotsHeight = slotsHeight;
        this.renderTextureFormat = renderTextureFormat;
        Debug.Log("SharedTextureManager::Initialized with constructor");
    }

    /// <summary>
    /// Returns a new SharedTextureSlot object
    /// </summary>
    /// <returns></returns>
    public SharedTextureSlot GetSharedTextureSlot()
    {
        // Create the new result object
        SharedTextureSlot sharedTextureSlot;

        // Try to find a free texture slot in the current list of SharedTextures and return it.
        for (int i=0;i<this.sharedTextureList.Count;i++)
        {
            for (int j=0;j< this.sharedTextureList[i].isUsable.Length;j++)
            {
                if (this.sharedTextureList[i].isUsable[j] == true)
                {
                    this.sharedTextureList[i].isUsable[j] = false;
                    sharedTextureSlot = new SharedTextureSlot(this.sharedTextureList[i],j, this);
                    return sharedTextureSlot;
                }
                    
            }
        }

        // If in the above query no free texture slot has been found, create a new SharedTexture, put it in the list and return it as result.
        Debug.Log("SharedTextureManager::No free slot in existing textures in sharedTextureList found. Adding new SharedTexture.");
        SharedTexture sharedTexture = new SharedTexture(this.width, this.slotsWidth, this.slotsHeight, this.renderTextureFormat);
        sharedTexture.isUsable[0] = false;
        this.sharedTextureList.Add(sharedTexture);
        sharedTextureSlot = new SharedTextureSlot(sharedTexture, 0, this);
        return sharedTextureSlot;
    }

    /// <summary>
    /// Releases the slot of the sharedTexture. Furthermore, if the texture is completely unused, it is nulled and removed from the sharedTextureList.
    /// </summary>
    /// <param name="sharedTextureSlot"></param>
    public void ReleaseSharedTextureSlot(SharedTextureSlot sharedTextureSlot)
    {
        // Release slot
        int slot = sharedTextureSlot.slot;
        sharedTextureSlot.sharedTexture.isUsable[slot] = true;
        // Check if housekeeping is possible
        if (sharedTextureSlot.sharedTexture.isTextureUsed() == false)
        {
            Debug.Log("SharedTextureManager::While releasing a SharedTextureSlot, the whole texture has been removed");
            sharedTextureSlot.sharedTexture.texture.Release(); 
            this.sharedTextureList.Remove(sharedTextureSlot.sharedTexture);
        }
        sharedTextureSlot = null;
    }
}

SharedTextureSlot

Code: Alles auswählen

using UnityEngine;
using System.Collections;

/// <summary>
/// Author: Joerg Zdarsky | joerg.zdarsky@gmx.de
/// Date: 2016-08-05
/// SharedTextureSlot is an instance of a shared texture (RenderTexture) which is managed in the background.
/// The dimension of the shared texture managed in the background is width x width*slots.
/// The idea is that, if e.g. multiple textures of 128x128 are required, these are stored in a single background texture in 128x128*slots texture.
/// The purpose is performance, as this single texture is uploaded once to the GPU and the GPU does not have to switch states when rendering multiple different objects.
/// </summary>
public class SharedTextureSlot : MonoBehaviour {

    public SharedTexture sharedTexture;
    public int slot;
    private SharedTextureManager sharedTextureManager;

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="sharedTexture"></param>
    /// <param name="slot"></param>
    /// <param name="sharedTextureManager"></param>
    public SharedTextureSlot(SharedTexture sharedTexture, int slot, SharedTextureManager sharedTextureManager)
    {
        this.sharedTexture = sharedTexture;
        this.slot = slot;
        this.sharedTextureManager = sharedTextureManager;
    }

    /// <summary>
    /// Releases the shared texture.
    /// </summary>
    public void Release()
    {
        this.sharedTextureManager.ReleaseSharedTextureSlot(this);
    }

    /// <summary>
    /// Returns the shared texture.
    /// </summary>
    /// <returns></returns>
    public RenderTexture Texture()
    {
        return this.sharedTexture.texture;
    }

    /// <summary>
    /// Returns the PixelOffset of the shared texture.
    /// </summary>
    /// <returns></returns>
    public Vector2 PixelOffset()
    {
        return this.sharedTexture.PixelOffset(this.slot);
    }

    /// <summary>
    /// Returns the scaling and offset in a vector4
    /// </summary>
    /// <returns>x contains X tiling dimension value, y contains Y tiling dimension value, z contains X offset value, w contains Y offset value.
    /// Tiling dimension is the amount a slot is smaller than the overall texture dimension in width and height direction.</returns>
    public Vector4 Offset()
    {
        return this.sharedTexture.Offset(this.slot);
    }
}

Den habe ich jetzt darüberhinaus noch so angepasst dass ich die Slots in Width und Height Richtung seperat festlegen kann. D.h. zum Beispiel einzelne Texturgrößen von sagen wir 256x256 können jetzt auch auf einer 1024x8192 Shared Textur abgebildet werden, das ergibt dann in dem Fall 128 Slots. Damit könnte ich das Batching in den Griff kriegen, bin langsam optimistisch.
Das Prinzip lässt sich an sich auf gleiche Variante für ComputeBuffer umsetzen. Das wäre als nächstes geplant sobald die Texturen funktionieren.
Zuletzt geändert von sushbone am 11.08.2016, 20:12, insgesamt 1-mal geändert.
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

So, die shared texture ist in die Asteroiden-Generierung eingebunden. Laut Debug-Konsole klappt das Anfordern von Textur-Slots und Anlegen von shared textures im Hintergrund. Auch die Pixel- und UVOffset-Werte pro Slot sehen gut aus. Schonmal gut. Auch dass der SurfaceShader jetzt nicht die ganze shared texture nutzt sondern nur einen Ausschnitt klappt. Letzte fehlende Anpassung ist nun noch die Berücksichtung des Offsets im ComputeShader und Vertex/Surface Shader. Man sieht am Bild dass immer in den ersten Slot geschrieben und gelesen wird, die Textur wiederholt sich. Das ist aber zu diesem Zeitpunkt das erwartete Ergebnis, weil ich aktuell noch Offset (0,0) verwende.
Sobald das angepasst ist sollte das ganze Konstrukt aber fliegen.
Dann könnten sich 16384 Planes eine Textur in 64x64 Auslösung or 4096 Planes in 128x128 Auflösung teilen.
Denn offset zu berücksichtigen sollte rasch ergänzt sein, dann geht es weiter damit die gleiche Strategie für die ComputeBuffer einzusetzen.

Wenn alles klappt sollte der Asteroid in einem Batch gerendert werden anstatt in tausenden.

Bild
Benutzeravatar
sushbone
Beiträge: 78
Registriert: 02.06.2013, 15:31

Re: [Projekt] Prozedurales Universum

Beitrag von sushbone »

Puuh auf den letzten Metern machen mir die Shader echt zu schaffen :-(.
Es will nicht funktionieren sobald die Shared Texture mehr als zwei Slots hat. Allerdings finde ich den Fehler nicht und mich wundert dass es erst ab zwei Slots nicht mehr geht.
Leider sind Shader nicht meine Stärke, aber meiner amateurhaften Meinung nach müssten ComputeShader und Surface-Shader passen.

Mit zwei Slots (16x32 für zwei 16x16 Texturen) sieht alles soweit OK aus.

Bild

Mit vier Slots (16x64 für vier 16x16 Texturen) fängt er scheinbar an in der Textur ins Leere zu greifen, die Normalen sehen wie (0,0,0)-Vektoren aus.

Bild

Dabei ist meines Erachtens alles OK. Folgende Shared Texture Slots werden erzeugt. Sieht soweit alles OK aus, Scale-Faktor (Größenverhältnis eines Slots im Vergleich zur gesamten Textur), UVOffset bzw. PixelOffset haben gute Werte. Habt ihr irgendeine Idee was das Problem sein könnte? Ich habe den ComputeShader irgendwie im Verdacht, ich interpretiere die schwarzen Flächen so dass der ComputeShader nicht die korrekten Bereiche mit normalen belegt und dort deswegen noch (0,0,0)-vektoren sind.
SharedTextureManager::No free slot in existing textures in sharedTextureList found. Adding new SharedTexture.
SharedTexture::Initialized with constructor. Overall texture dimension=16x64. Slots=4
SpaceObjectProcedural::Received new shared texture. Slot=0, Scale+Offset=(1.0, 0.25, 0.0, 0.0), PixelOffset=(0.0, 0.0)
SpaceObjectProcedural::Received new shared texture. Slot=1, Scale+Offset=(1.0, 0.25, 0.0, 0.25), PixelOffset=(0.0, 16.0)
SpaceObjectProcedural::Received new shared texture. Slot=2, Scale+Offset=(1.0, 0.25, 0.0, 0.5), PixelOffset=(0.0, 32.0)
SpaceObjectProcedural::Received new shared texture. Slot=3, Scale+Offset=(1.0, 0.25, 0.0, 0.75), PixelOffset=(0.0, 48.0)
SharedTextureManager::No free slot in existing textures in sharedTextureList found. Adding new SharedTexture.
SharedTexture::Initialized with constructor. Overall texture dimension=16x64. Slots=4
SpaceObjectProcedural::Received new shared texture. Slot=0, Scale+Offset=(1.0, 0.25, 0.0, 0.0), PixelOffset=(0.0, 0.0)
SpaceObjectProcedural::Received new shared texture. Slot=1, Scale+Offset=(1.0, 0.25, 0.0, 0.25), PixelOffset=(0.0, 16.0)
SpaceObjectProcedural::Received new shared texture. Slot=2, Scale+Offset=(1.0, 0.25, 0.0, 0.5), PixelOffset=(0.0, 32.0)
SpaceObjectProcedural::Received new shared texture. Slot=3, Scale+Offset=(1.0, 0.25, 0.0, 0.75), PixelOffset=(0.0, 48.0)
SharedTextureManager::No free slot in existing textures in sharedTextureList found. Adding new SharedTexture.
SharedTexture::Initialized with constructor. Overall texture dimension=16x64. Slots=4
SpaceObjectProcedural::Received new shared texture. Slot=0, Scale+Offset=(1.0, 0.25, 0.0, 0.0), PixelOffset=(0.0, 0.0)
SpaceObjectProcedural::Received new shared texture. Slot=1, Scale+Offset=(1.0, 0.25, 0.0, 0.25), PixelOffset=(0.0, 16.0)
SpaceObjectProcedural::Received new shared texture. Slot=2, Scale+Offset=(1.0, 0.25, 0.0, 0.5), PixelOffset=(0.0, 32.0)
SpaceObjectProcedural::Received new shared texture. Slot=3, Scale+Offset=(1.0, 0.25, 0.0, 0.75), PixelOffset=(0.0, 48.0)
SharedTextureManager::No free slot in existing textures in sharedTextureList found. Adding new SharedTexture.
SharedTexture::Initialized with constructor. Overall texture dimension=16x64. Slots=4
SpaceObjectProcedural::Received new shared texture. Slot=0, Scale+Offset=(1.0, 0.25, 0.0, 0.0), PixelOffset=(0.0, 0.0)
SpaceObjectProcedural::Received new shared texture. Slot=1, Scale+Offset=(1.0, 0.25, 0.0, 0.25), PixelOffset=(0.0, 16.0)
SpaceObjectProcedural::Received new shared texture. Slot=2, Scale+Offset=(1.0, 0.25, 0.0, 0.5), PixelOffset=(0.0, 32.0)
SpaceObjectProcedural::Received new shared texture. Slot=3, Scale+Offset=(1.0, 0.25, 0.0, 0.75), PixelOffset=(0.0, 48.0)
SharedTextureManager::No free slot in existing textures in sharedTextureList found. Adding new SharedTexture.
SharedTexture::Initialized with constructor. Overall texture dimension=16x64. Slots=4
SpaceObjectProcedural::Received new shared texture. Slot=0, Scale+Offset=(1.0, 0.25, 0.0, 0.0), PixelOffset=(0.0, 0.0)
SpaceObjectProcedural::Received new shared texture. Slot=1, Scale+Offset=(1.0, 0.25, 0.0, 0.25), PixelOffset=(0.0, 16.0)
SpaceObjectProcedural::Received new shared texture. Slot=2, Scale+Offset=(1.0, 0.25, 0.0, 0.5), PixelOffset=(0.0, 32.0)
SpaceObjectProcedural::Received new shared texture. Slot=3, Scale+Offset=(1.0, 0.25, 0.0, 0.75), PixelOffset=(0.0, 48.0)
SharedTextureManager::No free slot in existing textures in sharedTextureList found. Adding new SharedTexture.
SharedTexture::Initialized with constructor. Overall texture dimension=16x64. Slots=4
SpaceObjectProcedural::Received new shared texture. Slot=0, Scale+Offset=(1.0, 0.25, 0.0, 0.0), PixelOffset=(0.0, 0.0)
SpaceObjectProcedural::Received new shared texture. Slot=1, Scale+Offset=(1.0, 0.25, 0.0, 0.25), PixelOffset=(0.0, 16.0)
SpaceObjectProcedural::Received new shared texture. Slot=2, Scale+Offset=(1.0, 0.25, 0.0, 0.5), PixelOffset=(0.0, 32.0)
SpaceObjectProcedural::Received new shared texture. Slot=3, Scale+Offset=(1.0, 0.25, 0.0, 0.75), PixelOffset=(0.0, 48.0)
SharedTextureManager::No free slot in existing textures in sharedTextureList found. Adding new SharedTexture.
SharedTexture::Initialized with constructor. Overall texture dimension=16x64. Slots=4
SpaceObjectProcedural::Received new shared texture. Slot=0, Scale+Offset=(1.0, 0.25, 0.0, 0.0), PixelOffset=(0.0, 0.0)
SpaceObjectProcedural::Received new shared texture. Slot=1, Scale+Offset=(1.0, 0.25, 0.0, 0.25), PixelOffset=(0.0, 16.0)
Der ComputeShader erhält die PixelOffset-Informationen über einen ComputeBuffer, individual pro Oberflächen-Plane. Diese werden zum Thread-Index (16x16x1 Dispatch) des ComputeShaders ergänzt, wenn die Normalen-Vektoren in die Textur geschrieben werden.

Code: Alles auswählen

RWTexture2D<float4> patchGeneratedNormalMapTexture;

// Do normal calculation and create NormalMap and SurfaceTexture
void CSMain2 (uint2 id : SV_DispatchThreadID)
{
	// Get the constants
	GenerationConstantsStruct constants = generationConstantsBuffer[0];

	... calulcate normal....

	// Prepare Texture ID
	//uint2 textureID = uint2(id.y,id.x);
	uint2 textureID = uint2(id.y+constants.sharedNormalMapTextureSlotPixelOffset.x,id.x+constants.sharedNormalMapTextureSlotPixelOffset.y);

	// Create the ObjectSpace NormalMap
	....
	float3 normalRGB = normal.xyz /2;
	patchGeneratedNormalMapTexture[textureID] = float4(normalRGB,1);
}
Der Surface Shader bekommt die Scale- und UVOffset-Information über ein uniform float4.

Zuweisung über Unity:

Code: Alles auswählen

quadtreeTerrain.material.SetVector("_NormalMapOffset", quadtreeTerrain.sharedNormalMapTextureSlot.Offset());
Im Shader über "fixed3 normal = tex2D(_NormalMap, IN.uv_NormalMap * _NormalMapOffset.xy + _NormalMapOffset.zw);"

Code: Alles auswählen

Shader "Custom/ProceduralPatch" {
	Properties{
		_MaterialTex("Albedo (RGB)", 2D) = "white" {}
		_SurfaceMap("Albedo (RGB)", 2D) = "white" {}
		_NormalMap("Albedo (RGB)", 2D) = "bump" {}
	}
	SubShader{
		Tags{ "RenderType" = "Opaque" }
		LOD 200

		CGPROGRAM

		#include "UnityCG.cginc"

		#pragma surface surf Standard fullforwardshadows
		#pragma vertex vert
		
		struct appdata_full_compute {
			float4 vertex : POSITION;
			float4 tangent : TANGENT;
			float3 normal : NORMAL;
			float4 texcoord : TEXCOORD0;
			float4 texcoord1 : TEXCOORD1;
			float4 texcoord2 : TEXCOORD2;
			float4 texcoord3 : TEXCOORD3;
			#if defined(SHADER_API_XBOX360)
				half4 texcoord4 : TEXCOORD4;
				half4 texcoord5 : TEXCOORD5;
			#endif
				fixed4 color : COLOR;
			#ifdef SHADER_API_D3D11
				uint id: SV_VertexID;
			#endif
		};

		struct OutputStruct {
			float4 position;
			float3 patchCenter;
		};

		float globalNoise;

		#pragma target 5.0
		sampler2D _MaterialTex;
		sampler2D _SurfaceMap;
		sampler2D _NormalMap;

		#ifdef SHADER_API_D3D11
				StructuredBuffer<OutputStruct>	patchGeneratedFinalDataBuffer;
		#endif

		struct Input {
			float2 uv_MaterialTex;
			float2 uv_SurfaceMap;
			float2 uv_NormalMap;
			float3 worldPos;
			float3 objPos;
		};

		uniform float4 _NormalMapOffset; // E.g. (1.0,0.25,0.0,0.5) for the third 64x64 texture slot in a 64x265 texture 

		void vert(inout appdata_full_compute v, out Input o)
		{
			UNITY_INITIALIZE_OUTPUT(Input, o);
			#ifdef SHADER_API_D3D11
				// Read Data from buffer
				float4 position = patchGeneratedFinalDataBuffer[v.id].position;
				float3 patchCenter = patchGeneratedFinalDataBuffer[v.id].patchCenter;

				// Perform changes to the data
				// Translate the patch to its 'planet-space' center:
				position.xyz += patchCenter;

				// Apply data
				v.vertex = float4(position);
				o.uv_MaterialTex = v.texcoord.xy;
				o.uv_SurfaceMap = v.texcoord.xy;
				o.uv_NormalMap = v.texcoord.xy;
				o.worldPos = mul(unity_ObjectToWorld, v.vertex);
				o.objPos = v.vertex;
			#endif
		}
	
		void surf(Input IN, inout SurfaceOutputStandard o)
		{
			// Apply normalmap
			fixed3 normal = tex2D(_NormalMap, IN.uv_NormalMap * _NormalMapOffset.xy + _NormalMapOffset.zw);
			o.Normal = normal;

			// Apply materialtexture
			fixed3 crgb = tex2D(_MaterialTex, IN.uv_MaterialTex).rgb;
			fixed4 c = fixed4(crgb, 1);

			o.Albedo = c;
			o.Alpha = c.a;
			
		}
		ENDCG
	}
FallBack "Diffuse"
}
Mal schauen ich denke im Notfall werde ich nochmal den ComputeShader Code vereinfachen und nur einen Cube rendern, und mit den Shared Textures mal eine normale Farbtexturierung machen anstelle von normalen.
Dann kann man ggf. besser erkennen was genau passiert.
Antworten