Jump to content
Unity Insider Forum

Probleme mit Textur in ScriptableObject


Hellhound

Recommended Posts

Hallo zusammen,

ich habe aktuell mit meinem Previewer ein Problem was ich mir nicht erklären kann. Der Previewer ist ein ScriptableObject, der eine 2DTextur als SerializedField hält. Diese Textur wird in der OnEnable Methode erstellt, wenn sie noch nicht gesetzt ist. Das funktioniert soweit auch hervorragend, Betrachte ich das Asset des Scriptable Objects im Editor ist alles wie es sein soll, wie dieser Link zeigt. Nun starte ich jedoch die UnityEngine und hier tritt das Problem auf:

Der Debugger zeigt mir das OnEnable beim Start noch einmal aufgerufen wird. Am ScriptableObject sind alle Wert auch die Textur gesetzt, wie dieser Screenshot zeigt.

Wird nun jedoch das Item-Asset geladen, das kurz nach dem Aufruf von OnEnable geschiet, dann sind sämtliche Referenzen enthalten, sogar eine Referenz auf das serializierte FBX-Model, alleridngs sind die Objekt-Referenz Image und die Textur-Referenz texture null, wie dieser Screenshot zeigt.

Und das verstehe ich nicht. Beide Objekte sind als SerializedFields markiert, scheinen aber dies nur im Kontext des Editors zu sein. Muss ich bei Texturen etwas spezielles berücksichten wenn ich diese in einem ScriptableObject serialisieren will, die ich zuvor dynamisch erzeugt habe?

Gruß

Hellhound

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Sieht mir nach einem Reihenfolgeproblem aus, auch wenn du meinst "OnEnable" wäre schon durch. Ich verstehe auch nicht, warum du in der Awake-Methode des einen Skriptes bereits davon ausgehst, daß andere Skripte ihre Arbeit schon verrichret haben. Die Awake-Methode verwendet man eigentlich nur für Initialisierungen der eigenen Klasse, auf andere Objekte, vor allem auf solche bei denen noch etwas über klassenfremden Code generiert wird sollte man hier nicht zugreifen.
Ich würde in der Klasse "ItemDatabase" aus der Awake-Methode eine Start-Mehtode machen, damit ist OnEnable vermutlich tatsächlich durch und die Textur des Assets erstellt. Genau kann ich dir aber nicht sagen, was da passiert, dafür kenne ich deinen Aufbau nicht gut genug.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Ok verstehe was Du meinst, das werde ich morgen mal ausprobieren.

Was mich allerdings stutzig macht ist,dass beim OnEnable Aufruf beim Starten der Applikation des ScriptableObjects die Textur bereits gesetzt ist (wurde ja schon einmal im vor Programmstart editiert) und daher nicht noch einmal erstellt wird da sie ja bereits serialisiert ist. Beim Laden des Assets fehlt diese jedoch, die anderen serialisierten Werte sind jedoch da. Das ist ja der Vorteil des Scriptable Objects, einmal angelegt und über den Editor befüllt ist der Wert persistiert ... dachte ich zumindest ....

Link zu diesem Kommentar
Auf anderen Seiten teilen

Danke für Deinen letzten Hinweis. Habe heute Morgen gleich noch mal geschaut und dabei ist mir aufgefallen, das beim sauberen Start von Unity die Texturen ebenfalls zurückgesetzt sind, ich jedoch erwartet hätte, das sie serialisiert wurden. Irgendwie habe ich da jetzt generell nen Knoten im Kopf, daher versuche ich noch mal zu veranschaulichen, wie der konkrete Prozess ist (leider ist der Code zu umfangreich um ihn hier zu posten).

Hier einmal ein Klassendiagram zur aktuellen Struktur. D.h. ich habe ein ScriptableObject (Item), das selbst mehrere ScriptableObjects (ItemAttributes) enthält. Erzeugt wird das Item über den ItemEditor. Wird dem Item ein Attribut hinzugefügt, dann wird dies am Asset gespeichert:

// Draw a popup and button to add a new attribute:
EditorGUILayout.BeginHorizontal();
attributeNameIndex = EditorGUILayout.Popup(attributeNameIndex, attributeNames.ToArray(), GUILayout.Height(15));
if (GUILayout.Button("+", EditorStyles.miniButton, GUILayout.Width(20)))
{
	// A little tricky because we need to record it in the asset database, too:
	var newAttribute = CreateInstance(attributeNames[attributeNameIndex]) as ItemAttribute;
	newAttribute.ParentItem = item;
				
	newAttribute.hideFlags = HideFlags.HideInHierarchy;
	AssetDatabase.AddObjectToAsset(newAttribute, item);
	AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(newAttribute));
	AssetDatabase.SaveAssets();
	AssetDatabase.Refresh();
	item.attributes.Add(newAttribute);
}
EditorGUILayout.EndHorizontal();

Damit habe ich mein Asset (erster Screenshot AKM im Ressourcen-Verzeichnis) das die gehaltenen ScriptableObject ItemAttributes ebenfalls am Asset gebunden hat, die dynamisch über den ItemEditor hinzugefügt wurden. Das klappt soweit auch hervorragend. Alle Werte die ich bisher (Ausßnahme PreviewAttribut) im Inspector setzte, bleiben erhalten und sind sowohl nach dem Unity Neustart als auch beim Engine Start am AKM Asset serialisiert und beim Abruf über die ItemDatabase gesetzt.

Sonderfall ist jetzt nur das PeviewAttribut. Dieses erstellt beim erstmaligen Aufruf von OnEnable eine RenderTexture dessen Ergebnis in eine Texture2D überführt wird. Hier erstmal der OnEnable Aufruf:

#if UNITY_EDITOR
        public void OnEnable()
        {			
			var item = this.ParentItem;
			if(item!=null){
				var attr = item.GetAttribute<ModelData>();
				if(attr!=null && this.texture==null){			
					prefab = attr.Prefab;
					this.CreatePreview();
				}
			}
        }

Und hier die Erstellung der Preview:

      private void CreatePreview(){
            if (this.prefab != null){
				Debug.Log("Generate preview: " + prefab.name);
							
                string id = this.prefab.name;
                var renderTexture = new RenderTexture(1280, 720,(int)RenderTextureFormat.ARGB32);
                renderTexture.name = this.prefab.name;
				
                this.camera = this.SetUpCamera(renderTexture);
				this.camera.nearClipPlane = 0.03f;
				
			var center = this.camera.ScreenToWorldPoint(new Vector3(Screen.width/2, Screen.height/2, 0.4f));
				
                if (camera != null)
                {						
                    GameObject go = (GameObject)Instantiate(prefab);
                    go.transform.position =  (center + this.objectOffset);
                    EditorUtils.SetLayerRecursively(go, LayerMask.NameToLayer("Weapon"));
                    go.SetActive(true);
                    go.transform.rotation = Quaternion.Euler(0.0f, cameraRotation, 0.0f);
                    camera.Render();
					
                    RenderTexture.active = renderTexture;
                    Texture2D texture = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.ARGB32, false);
                    texture.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
                    texture.Apply();
                    RenderTexture.active = null;

                    this.texture = texture;
                    this.image = texture;
					
					// Encode texture into PNG
					byte[] bytes = this.texture.EncodeToPNG();
					File.WriteAllBytes(Application.dataPath + "Test-" + id + ".png", bytes);

                    GameObject.DestroyImmediate(go);
                    GameObject.DestroyImmediate(camera.gameObject);
                    //GameObject.DestroyImmediate(renderTexture);

                    AssetDatabase.SaveAssets();
                    AssetDatabase.Refresh();
                }							
			}
		}
	#endif

Ich habe testhalber auch mal eine PNG Erstellung mit eingebaut, die Textur wird erzeugt und liegt auch im Application-Root vor. Damit funktioniert zumindest das Rendern. Ich habe auch noch einmal auf Deinen Vorschlag hin das Asset gespeichert und aktualisiert, das ändert jedoch nichts ...

Wird Unity neu gestartet bzw. die Engine gestartet, dann ist im OnEnable Aufruf die zuvor gesetzte Textur-Referenz wieder null. Ich hätte jetzt erwartet, das diese serialisiert ist, da ja zuvor schon das PNG erstellt wurde. Und genau das verstehe ich jetzt nicht. Ok, ich könnte das ganze so umbbauen, das die PNG geladen wird, wenn vorhanden, aber eigentlich wollte ich das nur InMemory machen ohne explizite PNG Erstellung.

Kann es sein, das genau hier das Problem liegt? D.h. ich muss die Textur irgendwo speichern, da Unity intern nur Refernzen bei der Serialisierung hält  und die Texturdaten an sich nicht serialisiert sind? Bzw. Müsste ich die Textur explizit an das Asset binden, wie ich es mit dem ItemAttribut im Editor mache?

Gruß

Hellhound

P.S.: Was mich grad zusätzlich stutzig macht, ist das die Ergebnisse des Texturrenderns bei gleichen Werten unterschiedlich sind. Wenn ich die Textur erzeugt und ausgerichtet habe sieht das ganze so aus. Starte ich Unity neu dann wird beim neuen OnEnable-Aufruf mit gleichen Settings folgendes Resultat gerendert. Wie kann das sein?  Gleiche Werte, unterschiedliche Ergebnisse. Modifiziere ich dann egal welchen Wert im Inspector am Preview-Attribute und setze ihn anschließend zurück auf den Ausgangswert, habe ich wieder das ursprüngliche Render Ergebnis vor dem Neustart.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Nur schnell überflogen. Wo erzeugst du denn aus der Textur im Speicher ein Asset (außer das Speichern als PNG, ich denke hier wird ein Asset angelegt) und wo weist du das Asset dann dem SO zu? Ich vermute mal, wenn du das PNG dem SO zuweist könnte es klappen?
- AssetDatabase.LoadAssetAtPath // PNG
- "Asset dem SO zuweisen"

Link zu diesem Kommentar
Auf anderen Seiten teilen

Wo erzeugst du denn aus der Textur im Speicher ein Asset (außer das Speichern als PNG, ich denke hier wird ein Asset angelegt) und wo weist du das Asset dann dem SO zu?

Genau das mache ich nicht, das habe ich implizit erwartet und entspricht den Vermutungen im letzten Absatz vor dem Gruß. D.h. wenn ich Dich richtig verstehe, muss die Textur explizit persistiert und dem Asset zugeordnet werden, da vermutlich das Scriptable Objekt nur eine Refernz auf diese Zuordnung hält. Ich habe bisher nur das Preview-Attribut selbst dem Item SO Asset zugeordnet, wenn das Preview-Attribut erzeugt wird. Ich bin davon ausgegangen, das Unity die Textur implizit sichert, weil die Textur als SerializableField deklariert ist, so wie es sich mit den anderen Objekten, wie z.B. Vector3 usw. auch verhält, die binde ich ja auch nicht explizit an das Asset.

Das PNG war ja nur ein Zwischenschritt um zu Prüfen ob überhaupt was generiert wird. Dann muss ich mir das noch mal näher anschauen ...

Link zu diesem Kommentar
Auf anderen Seiten teilen

Ok, das scheint zumindest der richtige Ansatz zu sein. D.h. ich bekomme die Textur am Asset gesichert, wenn ich nach dem erstmaligen Preview-Aufruf das Asset-Binding durchführe:

public void OnEnable()
{			
	var item = this.ParentItem;
	if(item!=null){
		var attr = item.GetAttribute<ModelData>();
		if(attr!=null && this.texture==null)
        	{			
			prefab = attr.Prefab;
			this.CreatePreview();

        		AssetDatabase.AddObjectToAsset(this.texture, this);
        		AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(this.texture));
        		AssetDatabase.SaveAssets();
        		Debug.Log("Texture bind to Asset on creation.");
    		}
	}
}

Dann ist meine Textur am Asset sichtbar und wird auch beim Neustart von Unity erkannt, bzw. beim Start der Engine. Passt soweit. Nur wie bekomme ich es nun geupdated, wenn ich nachträglich im Inspector die Textur modifiziere?

Link zu diesem Kommentar
Auf anderen Seiten teilen

Schwer zu beantworten, da ich nicht genau nachvollziehen kann, wie genau du was machst. Jedenfalls ist ein SO immer ein Asset und kann selbst auch nur eine Referenz auf ein Asset halten. In deinem Fall muss dann die zur Laufzeit erzeugte Textur ebenfalls ein Asset werden. Wenn du dieses Asset nun modifizierst, dann muss Unity  "mitbekommen",  ich denke über:
https://docs.unity3d.com/ScriptReference/AssetDatabase.Refresh.html

Und vorher muss eben die geänderte Textur wiederum als Asset abgespeichert oder eben modifiziert werden.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Habs heut früh ausprobieren können, klappt. Du hattest recht so klappt es, allerdings musste ich vorher noch SaveAssets aufrufen. Zudem habe ich herausgefunden, dass man wirklich auf dem Objekt arbeiten muss, ändert man die Referenz, z.B. durch eine neue Instanz vom selben Typen und weist diese einfach zu, dann greifen die reinen AssetDatabase Mechanismen nicht, sondern man muss zuvor CopySerialized aufrufen. Letzteres hat mich einen Großteil der Zeit gekostet ...

Danke noch mal für Deine Tipps.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Archiviert

Dieses Thema ist jetzt archiviert und für weitere Antworten gesperrt.

×
×
  • Neu erstellen...