Wege zu perfekten 1-Pixel-Linien

Chiril

Welt-Boss
Mitglied seit
17.07.2008
Beiträge
1.226
Reaktionspunkte
1
Kommentare
53
Buffs erhalten
57
[font="verdana, sans-serif"]Dieses kleine Tutorial soll die Grundlagen der Erstellung von ein Pixel breiten Linien erklären und richtet sich damit eher an Anfänger, wenn auch Lua-Kenntnisse erforderlich sind. Vielleicht finden aber auch die einen oder anderen "Pros" hier etwas Neues
wink.gif
[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Ich denke, die meisten kennen es: Man will nur eben schnell im Interface eine kleine, feine Linie ziehen, die genau einen Pixel breit ist. Genau ein Pixel ist nicht zu dick, sondern genau richtig um einzelne Elemente einfach aber klar abzugrenzen. Kombiniert mit den richtigen Texturen und Schriftarten führt das (meist) zu einem sehr minimalistischem, aufgeräumtem und eigentlich sauberem Interface. „Eigentlich sauber"? Ja, leider. Blizard legt uns da ein paar Steine in den Weg und macht uns oft unnötig das Leben schwer. Ich möchte euch helfen, diese Steine aus dem Weg zu räumen und das Interface wirklich „sauber" zu machen, nicht eigentlich
wink.gif
[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Als erstes treffen wir ein paar Vorbereitungen, bevor wir zu den Linien an sich kommen.[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]1) Multisampling ausschalten.[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Ich mag eigentlich Multisampling. Dadurch werden die Kanten geglättet und das Spiel sieht besser aus. Aus mir unersichtlichen Gründen wird dadurch aber auch das Interface geglättet und saubere einen Pixel breite Linien fast unmöglich. Ich würde mich verdammt freuen, wenn ich irgendwann mal in den Patch Notes lesen würde, dass man das jetzt selber bestimmen kann, aber dazu wird es wohl nie kommen. Leider müssen wir jetzt die Entscheidung „Schönes Spiel oder schönes Interface?" treffen. Ich denke, die Antwort die ich voraussetze ich klar
wink.gif
[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]2) Die richtige UI-Skalierung[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Mit Hilfe der UI-Skalierung lässt sich die Größe jedes Elementes des Interfaces, oder besser, des Interfaces an sich, regulieren. Das ist an sich eine nette Idee und für das Standard Interface ein sehr tolles Feature. Wir wollen aber einen Pixel breite Linien auch als einen Pixel breite Linien im Spiel sehen und wenn wir sie durch die UI-Skalierung skalieren lassen würden, ginge das nicht. Wir setzen die Skalierung deshalb auf den Wert, der eine im Quellcode als „eins" angegebene Länge auch „eins" lang im Spiel darstellt.[/font]​
[font="verdana, sans-serif"]Der Wert lässt sich ganz einfach ermitteln: Wir teilen 768 durch die Y-Auflösung unseres Monitors. An meinem wäre es 768/1050, also etwa 0.73. Da uns „etwa" aber nicht reicht müssen wir den Bruch als solches irgendwo angeben. Dazu öffnen wir ingame den Chat und geben Folgendes ein:[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]
/run SetCVar('uiScale', 768 / 1050)
[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Das setzt die UI-Skalierung auf den Bruch, den wir gerne hätten.Wir können uns auch ein dynamisches Makro machen, das die Höhe automatisch anpasst (Danke an Wertzu):[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]
/script SetCVar("uiScale", 768/string.match(({GetScreenResolutions()})[GetCurrentResolution()], "%d+x(%d+)"))
(ungetestet)[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Nach Beachtung dieser beiden Punkte können wir endlich richtig losgehen. Endlich wird eine Linie, der wir im Code die Höhe „eins" zuweisen auch genau einen Pixel hoch. Wunderbar
smile.gif
[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Unsere Absicht ganz zu Anfang war etwas wie ein Rand, um unsere Elemente schön abzugrenzen. Da wir aber unterschiedliche Elemente haben, die es abzugrenzen gilt, müssen wir auch auf unterschiedliche Methoden, Wege und Funktionen zurückgreifen.[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Fangen wir mit der einfachsten an:[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]1) Die SetBackdrop()-Funktion[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Diese Funktion erstellt mit Hilfe der ihr zugewiesenen „bgFile" eine farbige Fläche hinter den Objekt auf das man sie anwendet. Ihren „insets" kann man dann Werte eintragen, die den „Überstand" von eigentlichen Objekt festlegen, also die Randdicke. Da wir alle faul sind machen wir es uns auch einfach:[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]
[font="verdana, sans-serif"]self:SetBackdrop{ bgFile = „Interface\\ChatFrame\\ChatFrameBackground", tile = true, tilesize= 16,[/font]​
[font="verdana, sans-serif"] insets = {left = -1, right = -1, top = -1, bottom = -1},}[/font]​
[font="verdana, sans-serif"]self:SetBackdropColor(0,0,0,1)​
[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Wir greifen dabei auf eine .blp von Blizzard zurück (Den ChatFrameBackground), da sie einfarbig ist uns für unsere Wünsche völlig reicht. Die Insets sind überall „-1", da wir einen Rand von einem Pixel haben wollen. Die Farbe lässt sich natürlich beliebig ändern, aber ich finde, dass alles außer schwarz doof aussieht
tongue.gif
.[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Diese Methode eignet sich am besten für Elemente, die undurchsichtig sind, also alle möglichen Leisten, wie UnitFrames, Timer und Ähnliches oder ganz einfache Frames.[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Ergebnis bei einer Leiste:[/font]​
[font="verdana, sans-serif"]http://i7chy.i7.funpic.de/wow/px/bar.png[/font]​
[font="verdana, sans-serif"][/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]2) Einen echten Rand erstellen[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Manchmal ist man allerdings auch in der Situation, dass man ein durchsichtiges Frame hat, dem man einen schicken Rand verpassen möchte. Dafür eignet sich eine Möglichkeit, die ich mal vor Ewigkeiten in einer nMinimap gefunden hab und die mittlerweile eh jeder benutzt.Wir nehmen hierfür eine Textur, zum Beispiel eine für einen Button und zerschneiden sie mit Hilfe der SetTexCoord()-Funktion in acht Stücke; vier Ecken und vier Kanten. Vorteil: Der Rand passt sich automatisch der Größe an und sieht immer „richtig" aus.[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"](Im Beispiel ist „m" das Frame, das den Rand bekommt)[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"][/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]local color = {r = 1, g = 1, b = 1}[/font]​
[font="verdana, sans-serif"]local scale = 1[/font]​
[font="verdana, sans-serif"]local pos = 1 –- dieser Wert gibt den Abstand vom Frame an[/font]​
[font="verdana, sans-serif"]local frameborder = "Interface\\AddOns\\AddonName\\media\\frameborder" –- selbstverständlich alles anpassen
wink.gif
[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]local TopLeft = m:CreateTexture(nil, "BORDER")[/font]​
[font="verdana, sans-serif"]TopLeft:SetTexture(frameborder)[/font]​
[font="verdana, sans-serif"]TopLeft:SetTexCoord(0, 1/3, 0, 1/3)[/font]​
[font="verdana, sans-serif"]TopLeft:SetPoint("TOPLEFT", m, -pos, pos)[/font]​
[font="verdana, sans-serif"]TopLeft:SetWidth(scale) TopLeft:SetHeight(scale)[/font]​
[font="verdana, sans-serif"]TopLeft:SetVertexColor(color.r,color.g,color.b)[/font]​
[font="verdana, sans-serif"]TopLeft:SetDrawLayer("BORDER")[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]local TopRight = m:CreateTexture(nil, "BORDER")[/font]​
[font="verdana, sans-serif"]TopRight:SetTexture(frameborder)[/font]​
[font="verdana, sans-serif"]TopRight:SetTexCoord(2/3, 1, 0, 1/3)[/font]​
[font="verdana, sans-serif"]TopRight:SetPoint("TOPRIGHT", m, pos, pos)[/font]​
[font="verdana, sans-serif"]TopRight:SetWidth(scale) TopRight:SetHeight(scale)[/font]​
[font="verdana, sans-serif"]TopRight:SetVertexColor(color.r,color.g,color.b)[/font]​
[font="verdana, sans-serif"]TopRight:SetDrawLayer("BORDER")[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]local BottomLeft = m:CreateTexture(nil, "BORDER")[/font]​
[font="verdana, sans-serif"]BottomLeft:SetTexture(frameborder)[/font]​
[font="verdana, sans-serif"]BottomLeft:SetTexCoord(0, 1/3, 2/3, 1)[/font]​
[font="verdana, sans-serif"]BottomLeft:SetPoint("BOTTOMLEFT", m, -pos, -pos+1)[/font]​
[font="verdana, sans-serif"]BottomLeft:SetWidth(scale) BottomLeft:SetHeight(scale)[/font]​
[font="verdana, sans-serif"]BottomLeft:SetVertexColor(color.r,color.g,color.b)[/font]​
[font="verdana, sans-serif"]BottomLeft:SetDrawLayer("BORDER")[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]local BottomRight = m:CreateTexture(nil, "BORDER")[/font]​
[font="verdana, sans-serif"]BottomRight:SetTexture(frameborder)[/font]​
[font="verdana, sans-serif"]BottomRight:SetTexCoord(2/3, 1, 2/3, 1)[/font]​
[font="verdana, sans-serif"]BottomRight:SetPoint("BOTTOMRIGHT", m, pos, -pos+1)[/font]​
[font="verdana, sans-serif"]BottomRight:SetWidth(scale) BottomRight:SetHeight(scale)[/font]​
[font="verdana, sans-serif"]BottomRight:SetVertexColor(color.r,color.g,color.b)[/font]​
[font="verdana, sans-serif"]BottomRight:SetDrawLayer("BORDER")[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]local TopEdge = m:CreateTexture(nil, "BORDER")[/font]​
[font="verdana, sans-serif"]TopEdge:SetTexture(frameborder)[/font]​
[font="verdana, sans-serif"]TopEdge:SetTexCoord(1/3, 2/3, 0, 1/3)[/font]​
[font="verdana, sans-serif"]TopEdge:SetPoint("TOPLEFT", TopLeft, "TOPRIGHT")[/font]​
[font="verdana, sans-serif"]TopEdge:SetPoint("TOPRIGHT", TopRight, "TOPLEFT")[/font]​
[font="verdana, sans-serif"]TopEdge:SetHeight(scale)[/font]​
[font="verdana, sans-serif"]TopEdge:SetVertexColor(color.r,color.g,color.b)[/font]​
[font="verdana, sans-serif"]TopEdge:SetDrawLayer("BORDER")[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]local BottomEdge = m:CreateTexture(nil, "BORDER")[/font]​
[font="verdana, sans-serif"]BottomEdge:SetTexture(frameborder)[/font]​
[font="verdana, sans-serif"]BottomEdge:SetTexCoord(1/3, 2/3, 2/3, 1)[/font]​
[font="verdana, sans-serif"]BottomEdge:SetPoint("BOTTOMLEFT", BottomLeft, "BOTTOMRIGHT")[/font]​
[font="verdana, sans-serif"]BottomEdge:SetPoint("BOTTOMRIGHT", BottomRight, "BOTTOMLEFT")[/font]​
[font="verdana, sans-serif"]BottomEdge:SetHeight(scale)BottomEdge:SetVertexColor(color.r,color.g,color.b)[/font]​
[font="verdana, sans-serif"]BottomEdge:SetDrawLayer("BORDER")[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]local LeftEdge = m:CreateTexture(nil, "BORDER")[/font]​
[font="verdana, sans-serif"]LeftEdge:SetTexture(frameborder)[/font]​
[font="verdana, sans-serif"]LeftEdge:SetTexCoord(0, 1/3, 1/3, 2/3)[/font]​
[font="verdana, sans-serif"]LeftEdge:SetPoint("TOPLEFT", TopLeft, "BOTTOMLEFT")[/font]​
[font="verdana, sans-serif"]LeftEdge:SetPoint("BOTTOMLEFT", BottomLeft, "TOPLEFT")[/font]​
[font="verdana, sans-serif"]LeftEdge:SetWidth(scale)[/font]​
[font="verdana, sans-serif"]LeftEdge:SetVertexColor(color.r,color.g,color.b)[/font]​
[font="verdana, sans-serif"]LeftEdge:SetDrawLayer("BORDER")[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]local RightEdge = m:CreateTexture(nil, "BORDER")[/font]​
[font="verdana, sans-serif"]RightEdge:SetTexture(frameborder)[/font]​
[font="verdana, sans-serif"]RightEdge:SetTexCoord(2/3, 1, 1/3, 2/3)[/font]​
[font="verdana, sans-serif"]RightEdge:SetPoint("TOPRIGHT", TopRight, "BOTTOMRIGHT")[/font]​
[font="verdana, sans-serif"]RightEdge:SetPoint("BOTTOMRIGHT", BottomRight, "TOPRIGHT")[/font]​
[font="verdana, sans-serif"]RightEdge:SetWidth(scale)[/font]​
[font="verdana, sans-serif"]RightEdge:SetVertexColor(color.r,color.g,color.b)[/font]​
[font="verdana, sans-serif"]RightEdge:SetDrawLayer("BORDER")[/font]​
[font="verdana, sans-serif"][/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Ergebnis bei einem transparenten Frame:[/font]​
[font="verdana, sans-serif"]http://i7chy.i7.funpic.de/wow/px/quadrat.png[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Die meisten erstellen für diese Methode eine eigene Funktion in einem Core-Addon, wo man all so ein Zeug reinpackt. BeautyCase von Neal zum Beispiel. Das sparrt eine Menge Code.[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Diese Methode lässt sich ausnahmslos überall erfolgreich anwenden.Eine sehr tückische Stelle sind dagegen Buttons. Auf Buttons wendet man für gewöhnlich (so wie zum Beispiel in rActionButtonStyler) Funktionen wie SetNormalTexture() oder SetCheckedTexture() an. Wir sind faul, also ist das völlig ok. Wenn wir dann den Button aber noch skalieren, wird unser toller 1-Pxiel-Skin (einer der tausend auf wowinterface, wie etwa Lunas, Skullflowers oder Qauns) schön unscharf. Wir haben dann nur die Möglichkeit, die Buttons einzeln ein halben Pixelschritten zu verschieben, solange bis sie scharf werden.[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Da wir aber alle faul sind, habe ich mich mal drangesetzt, die „Einen echten Rand erstellen" Methode in einem ActionBar-Addon zu verwirklichen. Bin auch relativ stolz auf das Ergebnis, der Rand ist nämlich perfekt
wink.gif
. Das Addon allerdings ... nicht wirklich
biggrin.gif
[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"]Das sind alle Wege, die ich zum Ziel perfekter einpixeliger Linien gehe und ich finde, dass ich schon fast am Ziel bin bin
smile.gif
Für Vorschläge, insbesondere Ergänzungen oder Korrekturen bin ich offen.[/font]​
[font="verdana, sans-serif"]
[/font]​
[font="verdana, sans-serif"](c) 2010, Chiril[/font]​
 
Zuletzt bearbeitet von einem Moderator:
Wenn du einen Guide posten willst kannst du dies gerne tun. Direktverlinkungen auf andere Seiten / Foren sind nicht erwünscht. Ich lass hier erstmal noch offen um dir die Gelegenheit zu geben deinen Post entsprechend anzupassen.
 
Zuletzt bearbeitet von einem Moderator:
Code:
local mult = SetCVar("uiScale", 768/string.match(({GetScreenResolutions()})[GetCurrentResolution()], "%d+x(%d+)"))/GetCVar("uiScale")
local function scale(x) return mult*x end

someframe:SetHeight(scale(10))
someframe:SetWidth(scale(10))

wenn man nicht die richtige auflösung benutzt

Pixelperfect test
wink.gif


Code:
-- Anti-aliasing test
local UnitsPerPixel = GetScreenWidth() * UIParent:GetScale() / select( GetCurrentResolution(), GetScreenResolutions() ):match( "%d+" );
local Px = UnitsPerPixel;


do
	local floor = math.floor;
	function TestRoundPixel ( X, Y )
	return Px * floor( X / Px + 0.5 ) + 0.5, Px * floor( Y / Px + 0.5 ) - 0.5;
	end
end




do
	local OnUpdate; -- Slowly moves the frame in a circle
	do
		local Scale, Cx, Cy = UIParent:GetScale(), UIParent:GetSize();
		Cx, Cy = Cx * Scale / 2, Cy * Scale / 2;
		local cos, sin = math.cos, math.sin;
		function OnUpdate( self, Elapsed )
			self.Angle = self.Angle + Elapsed * self.Rate;

			local X, Y = Cx + self.Radius * cos( self.Angle ), Cy + self.Radius * sin( self.Angle );
			if ( self.Round ) then
				X, Y = self.Round( X, Y );
			end

			self:SetPoint( "BOTTOMLEFT", X, Y );
		end
	end

	local Layers = {
		"BACKGROUND",
		"BORDER",
		"ARTWORK",
		"OVERLAY",
	};
	function Test ( Round, Radius, Size, Rate ) -- Creates an animated frame to visualize anti-aliasing
		local Frame = CreateFrame( "Frame" );
		Frame.Radius = Radius or 128;
		Frame.Size = Size or 128; -- In pixels
		Frame.Rate = Rate or math.pi / 240; -- 0.125 RPM default
		Frame.Round = Round;
		Frame.Angle = 0;

		Frame:SetSize( Frame.Size * Px, Frame.Size * Px );
		Frame:SetScript( "OnUpdate", OnUpdate );


		-- Create a pyramid pattern to make anti-aliasing stand out
		local LayerIndex = 0;
		local Top;
		for Index = 1, ceil( Frame.Size / 2 ) do
			LayerIndex = LayerIndex % #Layers + 1;
			if ( LayerIndex == 1 ) then -- Need new frame level
				Top = Top and CreateFrame( "Frame", nil, Top ) or Frame;
			end

			local Texture = Top:CreateTexture( nil, Layers[ LayerIndex ] );
			if ( Index % 2 == 1 ) then
				Texture:SetTexture( 0, 0, 0 );
			else
				Texture:SetTexture( 1, 1, 1 );
			end
			local Offset = Px * ( Index - 1 );
			Texture:SetPoint( "BOTTOMLEFT", Frame, Offset, Offset );
			Texture:SetPoint( "TOPRIGHT", Frame, -Offset, -Offset );
		end


		-- Add a close button
		local Close = CreateFrame( "Button", nil, Frame, "UIPanelCloseButton" );
		Close:SetFrameLevel( Top:GetFrameLevel() + 1 );
		Close:SetPoint( "CENTER" );
		Close:SetAlpha( 0 );
		Close:SetScript( "OnEnter", function ()
			Close:SetAlpha( 1 );
		end );
		Close:SetScript( "OnLeave", function ()
			Close:SetAlpha( 0 );
		end );

		return Frame;
	end
end


function Test1 () -- Creates one rounded and one unrounded frame that move side by side
	Test( false, 	128 ); -- No rounding
	Test( TestRoundPixel, 144 ); -- Snap to pixels
end
Test1();
 
Zuletzt bearbeitet von einem Moderator:
Zurück