Jump to content
Unity Insider Forum

Maleffekt erzeugen


minuschki

Recommended Posts

Hallo zusammen

Ich habe da wieder mal ein Problem, bei dem ich nicht weiss, ob es machbar ist und zwar möchte ich einen Maleffekt erzeugen!

Das Grundprinzip: Aus einem Quellbild (Sprite 2D Textur) wird die Farbe eines zufälligen Pixels ermittelt. Die Pinselform soll dann den Farbwert des Pixels annehmen und wird dann an der gleichen Position auf ein leeres Zielbild (Sprite 2d Textur) hinein kopiert werden. Da sich dieser Vorgang in einer Schleife x-mal wiederholt, würde sich das Zielbild mit weiteren Pinselstrichen allmählich füllen und schlussendlich ein "gemaltes" Bild erzeugen. Dabei ist es egal, wenn das eine Weile dauert. Soweit der Plan!

Ist sowas in Unity überhaupt machbar?

Für den 1. Schritt denke ich, dass man mit Texture2D.GetPixel den Farbwert bestimmen kann.

Das Umfärben der Pinselform ist mir aber ein Rätsel, geschweige denn, wie man die Pinselform ohne weissen Hintergrund in das Zielbild kopieren kann! Zudem wäre es natürlich notwendig, dass man den Pinselstrich punkto Skalierung, Rotation und Deckkraft steuern könnte, damit der Maleffekt einigermassen natürlich wirkt!

Ich bräuchte also so etwas: KOPIERE DEN PINSELSTRICH IN DER FARBE F, MIT DER ROTATION_R UND DER DECKKRAFT_D AUF DAS ZIELBILD AN DIE POSITION_X_Y

Sieht da jemand eine Lösung und kann mit weiterhelfen? Besten Dank im Voraus!

 

 

Pinsel.jpg

Link zu diesem Kommentar
Auf anderen Seiten teilen

Moin!

Der Name der Prozedur, mit der man eine Grafik von A nach B kopiert, heißt "Blit", und Unity bietet dir das an: Graphics.Blit

Ich bin mir aber leider nicht sicher, ob dir das Ding noch ernsthaft von Nutzen ist, sobald die Farbe ins Spiel kommt. Du müsstest vermutlich deine Pinseltextur ständig kopieren, die Kopie einfärben und dann auf dein Bild blitten. Du kannst mit GetPixels die Pinseltextur auslesen und dann mit SetPixels auf deine Kopie schreiben, dazwischen dann jedes Element des Arrays mit deiner ausgelesenen Farbe multiplizieren. Dazu sollte die Pindeltextur weiß sein. Anschließens einfach auf das Zielbild blitten. Pinsel drehen wird damit aber schwierig - das wird es aber sowieso.

Eine Alternative wäre, da eine Menge Mathematik draufzuschmeißen und das "händisch" zu machen. Du erstellst dir ein Color-Array mit so vielen Elementen, wie du Pixel haben willst und schreibst da rein. Du arbeitest da entweder mit einem eindimensionalen Array, machst ein bisschen Kopfschmerz-Mathe und schiebst das Ding am Ende 1:1 in deine Zieltextur, oder du machst ein zweidimensionales Array, arbeitest da etwas einfacher drauf und machst das Ding am Ende flach (also, schiebst die Daten in ein eindimensionales Array) bevor du es in die Textur schreibst.

Als letzte Möglichkeit, die zwar etwas unelegant ist, dafür aber sehr einfach und mächtig, könntest du einfach einen Haufen SpriteRenderer in eine Szene spawnen und davon ein Bild machen. Mit Camera.Render schreibst du das, was die Kamera sieht, in einer RenderTexture und bist fertig. Skalieren und Drehen des Pinsels ist damit extrem einfach, weil du nur die Transform-Komponenten deiner SpriteRenderer ansprechen musst.

Wenn du zu irgendeiner dieser Varianten Fragen hast, immer her damit.

 

P.S. Sehr schöner Thread. Problem vernünftig beschrieben, hilfreiche Bilder zur Veranschaulichung, konkrete Fragestellung. Das freut mich ein bisschen :D

Link zu diesem Kommentar
Auf anderen Seiten teilen

Hallo Sascha

Besten Dank für deine rasche Antwort, sogar am heiligen Sonntag, so quasi ausserhalb der Öffnungszeiten!

Von deinen vorgeschlagenen Möglichkeiten denke ich, dass die ersten beiden Varianten eher etwas für codeerfahrene Mathegenies und andere Halbgötter ist. Zudem äusserst du ja auch deine Bedenken bezüglich der Skalierung und Rotation des Pinselstriches. Aber die letzte Möglichkeit mit dem Spawnen von Sprites kann ich mir einigermassen vorstellen!

Dazu hätte ich noch folgende Fragen:

1. Ist es kein Problem, mit der Spawnmethode zwei- oder dreitausend Pinselstrich-Sprites in die Szene zu transferieren oder stösst Unity bzw. die Performance nach 100 oder mehr Spawns an seine Grenzen? Der zeitliche Aspekt spielt dabei weniger eine Rolle, aber die Anzahl der Striche schon.

Zum Pinsel:

Könntest du mir ein Codeschnipsel zeigen, wie man vom Prototyp-Pinsel eine Kopie erstellen und die Pinseltextur z.B. rot einfärben kann? Mir ist auch nicht klar, wie der Prototyp des Pinselstriches aussehen muss (schwarzer Strich auf weissem Grund oder umgekehrt im jpg-Format, oder schwarzer Strich auf transparentem Hintergrund oder weisser Strich auf transparentem Hintergrund im pgn-Format). Ich tippe mal auf einen schwarzen oder weissen Pinselstrich mit transparentem Hintergrund.

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 4 Stunden schrieb minuschki:

Ist es kein Problem, mit der Spawnmethode zwei- oder dreitausend Pinselstrich-Sprites in die Szene zu transferieren

Doch, das kann schon etwas knallen. Sowohl die Instanziierung als auch das Rendering. Dein Plan ist ja aber, einmalig ein Foto zu machen und den ganzen Kram danach wieder zu löschen. Das ganze läuft also innerhalb eines Frames ab, und dann kommt auch nix mehr. Hattest ja gesagt:

vor 9 Stunden schrieb minuschki:

Dabei ist es egal, wenn das eine Weile dauert.

Hättest du das nicht gesagt, hätte ich gesagt, da muss ein Compute Shader her, aber da kannste dann das "Halb" aus "Halbgott" streichen :P

Tatsache ist, dass Unity nicht mal eben so dem Ram überlasten wird. Und was Zeit angeht, wird Unity nicht abstürzen. Höchstens mal für ein paar Sekunden einfrieren :)

vor 4 Stunden schrieb minuschki:

Könntest du mir ein Codeschnipsel zeigen [...]

Jau: Du machst dir einfach ein Prefab und nutzt Instantiate.

public SpriteRenderer brushStrokePrefab;
var stroke = Instantiate(brushStrokePrefab, position, rotation);

position und rotation würdest du irgendwie ermitteln müssen. Position muss ja davon abhängen, wo dein Pixel ist, das du gerade verpinseln willst.

Danach dann:

stroke.color = pixelColor;

pixelColor ist die Farbe des gerade betrachteten Pixels. Die besorgst du dir in der Tat mit GetPixel.

Du baust dir also z.B. ein Quad mit einer festen Größe (am besten auch wieder ein Prefab anlegen). Das platzierst du und richtest die Kamera darauf. Wenn das Ding 1x1 groß ist, sparst du dir ein paar Rechenschritte :)

Dann liest du dir ein Pixel aus und errechnest die Position dieses Pixels für die Kamera. Dafür teilst du dir Position des Pixels durch die Dimensionen der "Leinwand", also deines Quads:

var pixelColor = texture.GetPixel(x, y);
var pixelPosition = new Vector2(x / texture.width, y / texture.height);

Es kann sehr gut sein, dass du bei y "1 - (y / texture-height)" schreiben musst, weil oben und unten gerne mal unterschiedlich sind bei Texturen und Kameras. Ich kann mir das immer nicht merken :P

Rotation kannst du mit Random würfeln.

vor 4 Stunden schrieb minuschki:

Ich tippe mal auf einen schwarzen oder weissen Pinselstrich mit transparentem Hintergrund.

Korrekt, ein weißer Strich. Farben sind ja einfach nur Zahlenwerte (in unserem Fall von 0 bis 1) für rot, grün, blau und alpha (0 = unsichtbar). Weiß ist also (1,1,1,1). Wenn du da z.B. rot (1,0,0,1) draufmultiplizierst, dann werden alle Komponenten miteinander multipliziert. Also (1 * 1, 1 * 0, 1 * 0, 1 * 1) = (1,0,0,1) = wieder rot. Und genau das macht die "color"-Eigenschaft des SpriteRenderers (wenn du keinen verrückten Cutsom-Shader drin hast): Draufmultiplizieren. Wäre dein Pinselstrich schwarz (0,0,0,1), kommt immer wieder schwarz heraus.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Hallo Sascha

Ich habe versucht deine Anregungen umzusetzen, komme aber mit getPixel und der Positionierung des Pinsels nicht ganz klar!

Um dem Problem auf den Grund zu gehen, habe ich das 1. Skript geschrieben. In einer Doppelschleife wird zeilenweise die Pixelfarbe ermittelt und dann in der Szene der Pinsel platziert (siehe Bild in der Mitte). Die Pinselform ist ein einfaches Quadrat. Das Resultat ist einigermassen korrekt. Komisch ist nur, dass, jedesmal, wenn ich die Doppelschleife erneut aufrufe, das Bildmotiv etwas hin- und herspringt. Dabei entstehen an den Rändern Pixelwiederholungen. Zudem ist mir aufgefallen, dass am unteren Rand immer eine Zeile hellgrüner Pixel sind, die eigentlich an den oberen Rand gehören. Warum dieser Effekt auftritt, ist mir nicht klar!

Das 2. Skript sollte eigentlich ein ähnliches Resultat zeigen, indem nach dem Zufallsprinzip Pixelfarben ausgelesen werden und an der gleichen Position der Pinsel platziert wird. Aber denkste: Das Resultat ist ein totaler Pixelsalat und das Motiv ist überhaupt nicht mehr zu erkennen (siehe Bild rechts).

Ich habe leider absolut keinen Schimmer, was da schiefläuft, denn eigentlich sollten doch beide Skripte praktisch dasselbe Resultat zeigen!

 

 

// Doppelschleife (Zeilenweises Auslesen der Pixel)

		for (int i = 0; i < 200; i = i + 2)//i++)
		{
			for (int k = 0; k < 200; k = k + 2)//k++)
			{
				farbe = tex.GetPixel (i, k);

				pos = new Vector2 (i / 50f, k / 50f);
				Quaternion winkel = Quaternion.Euler (0, 0, 0);
				Instantiate (pinsel, pos, winkel);
				pinsel.transform.localScale = new Vector3 (0.01f, 0.01f, 1); // Pinsel skalieren

				rend = pinsel.GetComponent<SpriteRenderer>();
				rend.color = farbe;
			}
		}




// Schleife mit Zufallspositionen

		for (int k = 1; k < anzahl; k++)
		{
			x = Random.Range (0, 200);
			y = Random.Range (0, 200);

			farbe = tex.GetPixel (x, y);

			pos = new Vector2 (x / 50f, y / 50f);
			Quaternion winkel = Quaternion.Euler (0, 0, 0);
			Instantiate (pinsel, pos, winkel);
			pinsel.transform.localScale = new Vector3 (0.01f, 0.01f, 1); // Pinsel skalieren

			rend = pinsel.GetComponent<SpriteRenderer>();
			rend.color = farbe;
		}

 

Beispiel.jpg

Link zu diesem Kommentar
Auf anderen Seiten teilen

Hat nen Moment gebraucht, aber hier ist das Problem:

Prefabs sind einfach nur GameObjects, die nicht in einer Szene sind, sondern in den Assets. Sonst gibt es (najaaaa... fast) keine Unterschiede zu den GameObjects in der Szene. Das beinhaltet, dass du Prefabs editieren kannst - dagegen sind sie nicht geschützt. Der Grund, warum der erste Code halbwegs funktioniert ist, dass du immer abwechselnd den Pinsel einfärbst und dann mit Instantiate "aufträgst". Du "tupfst" aber zuerst und änderst danach den Pinsel. Die Änderung des Pinsels wird dann erst beim nächsten Instantiate sichtbar, da der veränderte Pinsel in die Szene kopiert wird. Dein erstes Pixel-Objekt wird also die Ursprungsfarbe behalten, und das zweite kriegt dann die erste Pixelfarbe des Originalbildes.

Deine Doppelschleife geht ja nicht Zeile für Zeile, sondern Spalte für Spalte durch das Bild. Deshalb hat das erste Pixel (von unten) jeder Spalte die Farbe des letzten Pixels der vorherigen Spalte. Deshalb siehst du die oberste Zeile ganz unten. Und naja, dein Zufallsbild bricht dadurch gänzlich, da die Farbe des aktuellen Pixels von einem zufälligen anderen Pixel stammt.

Lange Rede, kurzer Sinn: Du willst nicht den Pinsel ändern, sondern die erstellte Kopie, also dein instanziiertes Pixel-Objekt:

farbe = tex.GetPixel (x, y);

pos = new Vector2 (x / 50f, y / 50f);
Quaternion winkel = Quaternion.Euler (0, 0, 0);
var pixelobjekt = Instantiate (pinsel, pos, winkel);
pixelobjekt.transform.localScale = new Vector3 (0.01f, 0.01f, 1); // Nicht den Pinsel skalieren, sondern das pixelobjekt

rend = pixelobjekt.GetComponent<SpriteRenderer>(); // Hier pixelobjekt benutzen statt pinsel
rend.color = farbe;

P.S. Gewöhne dir mal lokale Variablen an :)

Link zu diesem Kommentar
Auf anderen Seiten teilen

Archiviert

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

×
×
  • Neu erstellen...