Interpolationen bei der Rasterung (basierend auf Ebenen-Gleichung) - Texturierung und Gouraud-Beleuchtung von Dreiecken
-
Gegeben ist die Vektorgleichung der Ebene in 3D, mit Aufpunkt a und Richtungsvektoren
(b - a)
und(c - a)
.p = a + β·(b - a) + γ·(c - a)
Umstellen ergibt
p - a = β·(b - a) + γ·(c - a)
Auflösung nach β und γ mit der Cramerschen Regel und dem Trippelprodukt
<e3, a x b>
was der Determinantedet(a, b)
entspricht.
2D Kreuzprodukt eben.Bekomme ich
D := <e3, (b - a) x (c - a)> (also die dritte Komponente vom 3D-Kreuzprodukt)
D1 := <e3, (p - a) x (c - a)>
D2 := <e3, (b - a) x (p - a)>Und letztlich
β = D1 / D
γ = D2 / DDann rechne ich noch α aus
α = 1 - β - γ
Und zeichne ein Pixel nur, wenn
α >= 0
,β >= 0
undγ >= 0
.Mit
D
kann man auch noch die Orientierung des Dreiecks (gegen Uhrzeigersinn = positiv, im Uhrzeigersinn = negativ) testen.
Dann kann man nur die Dreiecke, die entgegen dem Uhrzeigersinn sind, zeichnen.Und darauf basiert nun meine
ComputeArealCoordinates
-Funktion hier:void Rasterize(Tuple2 const& a, Tuple2 const& b, Tuple2 const& c, DrawPixelFunction* draw_pixel) { Rectangle const kBbox = ComputeBbox(a, b, c); for (auto y = kBbox.kMinY; y < kBbox.kMaxY; ++y) { for (auto x = kBbox.kMinX; x < kBbox.kMaxX; ++x) { Tuple2 const kBetaAndGamma = ComputeArealCoordinates(Tuple2{static_cast<float>(x), static_cast<float>(y)}, a, b, c); float const kBeta = kBetaAndGamma.x; float const kGamma = kBetaAndGamma.y; float const kAlpha = 1.0f - kBeta - kGamma; if (kAlpha >= 0.0f && kBeta >= 0.0f && kGamma >= 0.0f) { draw_pixel(x, y, 255, 255, 255); } } } }
Frage 1: Texturen (mit Blick auf den Ansatz oben)
Nehmen wir mal an, ich habe eine Funktion
GetRgbColorFromTexture(u, v)
, die mir ein RGBstruct
liefert.struct RgbColor { uint8_t red; uint8_t green; uint8_t blue; }
Nehmen wir mal auch an, dass die Texturkoordinaten (durch die OBJ-Datei) gegeben sind.
Für Vertex a ist das der 2D-Tupel (ta.x, ta.y), für b (tb.x, tb.y), für c (tc.x, tc.y).Dann würde ich die Vektor-Ebenengleichung nochmal mit den Texturkoordinaten aufstellen:
p = β * (tb - ta) + γ * (tc - ta) + ta
p = (1 - u - v) * ta + γ * tb + γ * tc
p = α * ta + β * tb + γ * tcAlso, bekomme ich den richtigen Punkt p (u, v) von der Textur, mit der ich dann die Farbe bestimmen kann.
if (kAlpha >= 0.0f && kBeta >= 0.0f && kGamma >= 0.0f) { Tuple2 pixel_position = kAlpha * ta + kBeta * tb + kGamma * tc RgbColor rgb_color = GetRgbColorFromTexture(pixel_position.x, pixel_position.y); draw_pixel(x, y, rgb_color.red, rgb_color.green, rgb_color.blue); }
Stimmt das so weit?
Frage 2: Gouraud-Beleuchtung
Hier braucht man 3 Normalvektoren.
Nennen wir sie mal na, nb, nc.N := α * na + β * nb + γ * nc
Also wie bei den Texturen, nur dass da jetzt ein
Tuple3
(x, y, z) bzw. einTuple4
(x, y, z, w) (verwendet wird)?N
da oben soll die resultierende Normale sein.Wenn ich nur eine Normale hätte
n0
, kann ich dann einfachN = α * n0 + β * n0 + γ * n0
... machen?
Zum Schluss, würde ich noch ein einfaches Beleuchtungsmodell nehmen
<N, L> und gucken, dass das nicht unter 0 geht.
Somit habe ich den Faktor für die Intensität
<N, L>
.if (kAlpha >= 0.0f && kBeta >= 0.0f && kGamma >= 0.0f) { Tuple2 pixel_position = kAlpha * ta + kBeta * tb + kGamma * tc Tuple4 normal = kAlpha * na + kBeta * nb + kGamma * nc; RgbColor rgb_color = GetRgbColorFromTexture(pixel_position.x, pixel_position.y); auto intensity = Dot(normal, light_direction); draw_pixel(x, y, rgb_color.red * intensity, rgb_color.green * intensity, rgb_color.blue * intensity); }
draw_pixel(x, y, rgb_color.red * intensity, rgb_color.green * intensity, rgb_color.blue * intensity);
Macht das hier Sinn, die Lichtfarbe noch zu vermerken?
Alsodraw_pixel(x, y, rgb_color.red * light_color.red * intensity, rgb_color.green * light_color.green * intensity, rgb_color.blue * light_color.blue * intensity);
-
Hallo dozgon,
1: Wenn du Texturkoordinaten über ein Dreieck interpolieren willst, was du über eine Kamera mit perspektivischer Projektion betrachtest, dann musst du die Koordinaten perspektivisch korrigieren indem du sie erst durch die Z-Koordinate des Dreiecks dividierst, dann bayzentrisch interpolierst (sie wie du es schon machst) und dann wieder durch Z dividierst. Details dazu findest du hier: https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/perspective-correct-interpolation-vertex-attributes
2: Normalen kannst du ohne diesen 1/z-Trick interpolieren. Du musst sie dann nur nochmal normalisieren.
Wenn du nur ein Normale für ein Dreieck hast (Flatshading) dann darst du sie nicht interpolieren. Du musst sie dir dadurch errechnen, indem du das Kreuzprodukt mit zwei Kanten des Dreiecks bildest.
3: Wenn du farbige Lichtquellen willst, dann macht es Sinn die Lichtfarbe mit in die Formel mit reinzunehmen. Wenn du nur weißes Licht willst, dann kannst du es weglassen (Variante 1)
Ich würde die raten dieses Tutorial von Scratchapixel einfach mal anzusehen. Gerade was das Rastern angeht. Eigentlich macht man das so, dass man über lauter Pixel iteriert und für jede Pixelmitte schaut, ob es innerhalb vom 2D-Dreieck liegt. Das macht man mit der Edge-Funktion wie in den Tutorial. Außerdem musst du aber für korrekte Darstellung die Top-Left-Regel und die Pixel-Center -Regel beachten (Siehe hier: https://docs.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-rasterizer-stage-rules) da du sonst Löcher bekommst, wenn du mehrere Dreiecke zeichnest, dessen Kanten sich berühren.
-
Hallo XMAMan!
- Danke für den Hinweis mit perspektivischer Korrektur!
Ich habe 2 Projekte, einmal mit C und einmal mit C++.
Bei dem C-Projekt möchte ich es wie bei der PlayStation 1 haben (also ohne perspektivischer Korrektur).
Bei dem C++-Projekt mit.
Soweit ich gesehen habe, hat das jetzt mit der Beleuchtung geklappt.
Habe die Beleuchtung jetzt mal vorübergehend aus, da ich noch aktuell mit Texturen am Hadern bin.Das da unten ist vom C Projekt:
Nur leider ist die Textur nicht richtig abgebildet aus irgendeinem mir noch unbekannten Grund.
Ich spekuliere mal, dass es an meinem Rasterizer liegt, der mit Festpunktzahlen arbeitet (16-Bit für die Nachkommastellen.)static unsigned char* ComputeTexelColor(Texture* texture, float alpha, float beta, float gamma, float u0, float v0, float u1, float v1, float u2, float v2) { float const kInterpolatedU = alpha * u0 + beta * u1 + gamma * u2; float const kInterpolatedV = alpha * v0 + beta * v1 + gamma * v2; int const kWidth = Texture_get_width(texture); int const kHeight = Texture_get_height(texture); int const kBytesPerPixel = Texture_get_bytes_per_pixel(texture); unsigned char* texels = Texture_get_data(texture); int const position_x = (int)(kInterpolatedU * kWidth); int const position_y = (int)(kInterpolatedV * kHeight); return texels + ((position_x + position_y * kWidth) * kBytesPerPixel); }
Der Code sieht für mich in Ordnung aus.
Was ich hier mache, ist einfach mit den UV-Koordinaten und den baryzentrischen Koordinaten (vom Rasterizer) zusammenrechnen (oder interpolieren).
Dann aus der Textur (also der Bilddatei), die Farben raus extrahieren.
Die Texturbildpositionenposition_x
undposition_y
, die ich beim Debuggen gesehen habe, waren auch in Ordnung.static void ComputeColor(long alpha, long beta, long gamma, signed int shift_amount, unsigned char (*colors)[3], void* data) { float const kReciprocal = 1.0f / (float)((1L << shift_amount)); float const kAlpha = (float)alpha * kReciprocal; float const kBeta = (float)beta * kReciprocal; float const kGamma = (float)gamma * kReciprocal; TextureHandle texture = g_game.texture_data[0]; TriangleFace* face = (TriangleFace*)data; /* ... */ { unsigned char* color = ComputeTexelColor( texture, kAlpha, kBeta, kGamma, face->vertices[0].texture_coordinates.x, face->vertices[0].texture_coordinates.y, face->vertices[1].texture_coordinates.x, face->vertices[1].texture_coordinates.y, face->vertices[2].texture_coordinates.x, face->vertices[2].texture_coordinates.y); (*colors)[0] = (unsigned char)(kClampedLightIntensity * color[0]); (*colors)[1] = (unsigned char)(kClampedLightIntensity * color[1]); (*colors)[2] = (unsigned char)(kClampedLightIntensity * color[2]); } }
Das da oben ist die Funktion, die ich dem Rasterizer gebe.
Also hier werden die Farben (interpoliert) ermittelt für das Licht und die Textur.Dann sind wir hier bei dem Rasterizer:
void RasterizeTriangle(int ax, int ay, int bx, int by, int cx, int cy, ComputeColorFunction* compute_color, DrawPixelWithColorFunction* draw_pixel, void* data) { /* W1 := OB - OA */ int const kW1X = bx - ax; int const kW1Y = by - ay; /* W2 := OC - OA */ int const kW2X = cx - ax; int const kW2Y = cy - ay; /* Det(w1, w2) = <w1 x w2, e3> = |w1| * |w2| * sin(α) */ int const kD = COMPUTE_DETERMINANT(kW1X, kW1Y, kW2X, kW2Y); /* Cue: |w1| * |w2| * sin(α) */ int const kDegenerate = kD == 0; int rectangle[4] = ZERO4; /* Ignore degenerate triangles. */ if (kDegenerate == 1) { return; } ComputeBbox(ax, ay, bx, by, cx, cy, &rectangle); { /* Minimum bounding rectangle. */ int const kMinX = rectangle[0]; int const kMaxX = rectangle[1]; int const kMinY = rectangle[2]; int const kMaxY = rectangle[3]; /* Cue: float kDReciprocal = 1.0f / (float)kD; */ long const kReciprocal = DIVIDE_AND_SCALE(SCALED_ONE, TO_FIXED_POINT(kD)); int y = 0; int x = 0; for (y = kMinY; y < kMaxY; ++y) { for (x = kMinX; x < kMaxX; ++x) { /* P - V0 */ int const kWX = x - ax; int const kWY = y - ay; /* Det(w, w2) = <w x w2, e3> = |w| * |w2| * sin(α) */ int const kD1 = COMPUTE_DETERMINANT(kWX, kWY, kW2X, kW2Y); /* Det(w1, w) = <w1 x w, e3> = |w1| * |w| * sin(α) */ int const kD2 = COMPUTE_DETERMINANT(kW1X, kW1Y, kWX, kWY); /* kS1 := D1 / D */ long const kS1 = MULTIPLY_AND_SCALE(TO_FIXED_POINT(kD1), kReciprocal); /* kS2 := D2 / D */ long const kS2 = MULTIPLY_AND_SCALE(TO_FIXED_POINT(kD2), kReciprocal); /* kS3 := 1 - S1 - S2 */ long const kS3 = SCALED_ONE - kS1 - kS2; /* Hint: (kS1 | kS2 | kS3) >= 0 ~ kS1 >= 0 && kS2 >= 0 && kS3 >= 0 */ int const kPointInside = (kS1 | kS2 | kS3) >= 0L; if (kPointInside == 0) { continue; } /* Draw pixel. */ { unsigned char colors[3] = {0, 0, 0}; unsigned char* color = colors; compute_color(kS1, kS2, kS3, FRACTIONAL_BITS, &colors, data); draw_pixel(x, y, color[0], color[1], color[2]); } } } } }
Das Culling (also die Rückseitenentfernung, wenn die Dreiecke im Uhrzeigersinn orientiert sind), habe ich hier im Rasterizer entfernt.
Ich habe das Culling dann nach dem Verrechnen der Vertexpositionen (Modelltransformationen wie Rotation, Translation, Skalierung) hinzugefügt.Das mit Festkommazahlen sieht so aus:
#define FRACTIONAL_BITS 16L #define SCALED_ONE (1L << FRACTIONAL_BITS) #define TO_FIXED_POINT(x) ((long)(x) << FRACTIONAL_BITS) #define TO_INTEGER(x) ((long)(x) >> FRACTIONAL_BITS) #define MULTIPLY_AND_SCALE(x, y) (((long)(x) * (long)(y)) >> FRACTIONAL_BITS) #define DIVIDE_AND_SCALE(x, y) (((long)(x) << FRACTIONAL_BITS) / (long)(y))
Also kein Hexenwerk.
Wenn ich den Bug immer noch nicht finde (habe schon mehrere Stunden mit dem Debugger verbracht), dann versuche ich es nochmal mit Gleitpunktzahlen.
Ich habe mal versucht, einfach hier mit der ein Halbes zu addieren (um die richtige Pixelposition nach der Umwandlung von Welt zu Pixelkoordinaten zu bekommen):
#define SCALED_ONE_HALF (1L << (FRACTIONAL_BITS - 1L)) /* ... */ /* P - V0 */ int const kWX = TO_FIXED_POINT(x - ax) + SCALED_ONE_HALF; int const kWY = TO_FIXED_POINT(x - ax) + SCALED_ONE_HALF;
Hat aber nicht zum Erfolg geführt (also, dass die Textur korrekt abgebildet wird).
Ich habe eigentlich die Rendering-Pipeline in dem C-Projekt fast fertig.
Es fehlen Clipping (also wegschneiden von unsichtbaren Polygonen) und halt die Texturen.
Und dann muss ich noch viel umschreiben oder "refactoren", da einige Code-Stellen noch "hässlich" aussehen.
Im C++-Projekt fehlt noch perspektivische Korrektur, Shadow-Rays, Reflection-Rays, Refraction-Rays und das Clipping.
Beim C++-Projekt verwende ich zwei Bildsynthese-Verfahren: Ray-Tracing und Rasterization.Edit:
Also ich verstehe das nicht ganz.
Ich habe eine ganz normale box.obj geladen.
Und die Texturen scheinen hier okay zu sein.
Aber bei komplexeren .OBJ-Modellen halt nicht:
- Danke für den Hinweis mit perspektivischer Korrektur!
-
Der Fehler mit der Textur ist nun fast gelöst.
https://i.postimg.cc/mDSzyPRd/Screenshot-from-2022-09-12-19-23-12.png
Wie man hier sieht, ist die Texturierung jetzt korrekter.
Aber immer noch ein Fehler im Torso-Bereich von Claude Speed.static unsigned char* ComputeTexelColor(Texture* texture, float alpha, float beta, float gamma, float u0, float v0, float u1, float v1, float u2, float v2) { float const kInterpolatedU = alpha * u0 + beta * u1 + gamma * u2; float const kInterpolatedV = alpha * (1.0f - v0) + beta * (1.0f - v1) + gamma * (1.0f - v2); int const kWidth = Texture_get_width(texture); int const kHeight = Texture_get_height(texture); unsigned char* texels = Texture_get_data(texture); int const position_x = CLAMP((int)(kInterpolatedU * kWidth), 0, kWidth); int const position_y = CLAMP((int)(kInterpolatedV * kHeight), 0, kHeight); return texels + ((position_x + position_y * kWidth) * 4); }
Also der Fehler war, dass die Texturkoordinaten von Claude von oben beginnen und unten enden.
Daher muss die y-Achse der Texturkoordinaten angepasst bzw. gespiegelt werden.float const kInterpolatedV = alpha * (1.0f - v0) + beta * (1.0f - v1) + gamma * (1.0f - v2);
Das ist wahrscheinlich Konventionssache, aber viele der Modelle, die ich getestet habe, wenden diese Konvention an.
In Ordnung, ich habe meine Frage fast selbst beantwortet.
Aber nur noch ein Bug in seinem Torso haelt mich davon ab weiter zu kommen.Nachtrag:
Hab inzwischen den anderen Fehler auch gefunden.
Ich muss natuerlich die Werte am Rand von Breite und Hoehe abschneiden und zwar so:int const position_x = CLAMP((int)(kInterpolatedU * kWidth), 0, kWidth - 1); int const position_y = CLAMP((int)(kInterpolatedV * kHeight), 0, kHeight - 1);
Da ich wohl mit Festkommazahlen (also ganzen Zahlen rechne), habe ich wohl noch Rundungsfehler, die evtl. auch dazu Beitragen, dass die Textur noch nicht ganz Perfekt ist.
Zudem habe ich nicht darauf geachtet das gemeinsame Dreieckskanten nicht doppelt gezeichnet werden...
-
Du schreibst eine Grafikengine die Raytracing+Rasterizer unterstützt? So ein Zufall. Ich habe da auch so ein Projekt, an dem ich schon lange dran rumbastle was auch OpenGL/DirectX/Software-Rasterizer/Raytracing unterstützt. Würde mich mal interessieren, was für Bilder dein Projekt dann so erzeugt. Ich selbst überarbeite gerade meine Engine und werde sie in kürze dann auf Github mal hochladen.
Wenn du das Clipping noch implementieren musst, dann empfehle ich dir:
https://fabiensanglard.net/polygon_codec/
In der Methode ClipPolygonOnWAxis muss die Zeile
intersectionFactor = (W_CLIPPING_PLANE - (*previousVertice)[W] ) / ((*previousVertice)[W]- (*currentVertice)[W]);dann noch zu
intersectionFactor = ((*previousVertice)[W] ) / ((*previousVertice)[W]- (*currentVertice)[W]);
geändert werden und dann geht es.
-
@XMAMan Ich freue mich Dich kennenlernen zu dürfen.
Danke für den Link und die Hinweise!
Auf meinem GH-Profil, findest Du
GRender
(Software-Renderer) und meinedozgon engine
(Echtzeit-Software-Renderer im PS1-Stil).Zum PS1-Renderer:
Die Übersetzungseinheiten ("translation units" in der C- und C++-Welt) im Verzeichnis
game/
, die noch ausgebessert werden müssen, wären:game.c
game.h
model.c
model.h
Der Code hier ist nicht sauber und ich finde den sehr hässlich und da gibt es noch Sachen, die ich vorberechnen könnte, um Rechenzeit zu sparen.
Die Verzeichnisse
standalones
undfreestanding
(frei stehende Umgebung heißt so viel wie läuft ohne Betriebssystem) sind modular und unabhängig voneinander.Also ich könnte in den Ordner
freestanding
schauen und mirrasterizer.c
undrasterizer.h
herausnehmen und irgendwo anders sehr leicht integrieren.
-
Hallo @XMAMan, ich muss das Thema leider nochmal aufgreifen, da Lara Croft (PS1-OBJ-Datei) mit Gouraud-Schattierung noch nicht so gut aussieht.
Also ich habe in meinem Software-Renderer (im PS1-Stil) jetzt zwei Schattierungsroutinen:
- Flat Shader
float ComputeFlatShading(Tuple4 const* surface_normal, Tuple4 const* light_direction) { float const kIntensity = DotTuple4(surface_normal, light_direction); float const kResult = CLAMP(kIntensity, 0.25f, 1.0f); return kResult; }
- Gouraud Shader
float ComputeGouraudShading(Tuple4 const* normal_a, Tuple4 const* normal_b, Tuple4 const* normal_c, Tuple4 const* light_direction, float alpha, float beta, float gamma) { float const kIntensityA = DotTuple4(normal_a, light_direction); float const kIntensityB = DotTuple4(normal_b, light_direction); float const kIntensityC = DotTuple4(normal_c, light_direction); float const kInterpolatedIntensity = (kIntensityA * alpha + kIntensityB * beta + kIntensityC * gamma); float const kResult = CLAMP(kInterpolatedIntensity, 0.25f, 1.0f); return kResult; }
Ich glaube der Flat Shader ist in Ordnung.
Es ist einfach nur die Oberflächennormale skalar multipliziert mit der Lichtrichtung.Bei Gouraud, bin ich mir nicht sicher ob das so stimmt.
Wie Du siehst, verrechne ich zunächst, die Lichtintensitäten pro (Dreiecks-)Vertex (A, B, C).
Dann interpoliere ich die einzelnen Intensitäten mit den baryzentrischen Koordinaten (Alpha, Beta, Gamma) vom Rasterizer.
Und dann lasse ich nur Werte oder Intensitäten im Bereich von [0.25; 1.0f] zu.Die Normalen berechne ich nicht extra, sondern hol diese von der OBJ-Datei direkt (also die
vn
-Sachen).Warum ist das offenbar falsch?
Erinnerung:
void RasterizeTriangle(int ax, int ay, int bx, int by, int cx, int cy, ComputeColorFunction* compute_color, DrawPixelWithColorFunction* draw_pixel, void* optional);
So sieht die Signatur vom Rasterizer aus.
ComputeColorFunction
liefert Farbwerte mittels Interpolation.
Das wird dann innerhalb derRasterizeTriangle
-Funktion so verwendet:/* Draw pixel. */ { unsigned char colors[3] = {0, 0, 0}; unsigned char* color = colors; compute_color(kAlphaScaled, kBetaScaled, kGammaScaled, kFractionalBits, &colors, data); draw_pixel(x, y, color[0], color[1], color[2]); }
Data zeigt auf ein
TriangleFace
, also:struct Vertex { Tuple4 color; /* RGBA (float) */ Tuple4 position; Tuple4 texture_coordinates; Tuple4 vertex_normal; }; struct TriangleFace { Tuple4 color; /* RGBA (float) */ Vertex vertices[3]; };
-
Hallo dozgon,
ich konnte dein Fehler mit mein Rasterizer nachstellen:
https://i.ibb.co/yRgmvqk/Dot-Lighting-Pixel-Shader-vs-Vertex-Shader.jpg
Wenn du im VertexShader die Farbe berechnest, dann beachtest du nicht, dass ja die "ToLight"-Richtung falsch ist.
Im VertexShader schreibe ich
Vector3D lightPosition = new Vector3D(0, 7.5f, 3); Vector3D toLight = Vector3D.Normalize(lightPosition - outputVertex.Position); Vector3D lam = new Vector3D(1, 1, 1) * System.Math.Max(outputVertex.Normal * toLight, 0.0f); shaderOutput.Interpolationvariables.AddVector3D(lam, Interpolationvariables.VariableType.WithoutPerspectiveDivision);
Im PixelShader schreibe ich
Vector3D toLight = Vector3D.Normalize(prop.Lights[0].Position - v.Position); return new Vector4D(new Vector3D(1, 1, 1) * Math.Max(normalVector * toLight, 0.0f), 1);
Wenn ich im PixelShader rechne, dann bekommt ja toLight stehts den "echten" Wert und wenn ich im VertexShader das mache, dann interpoliere ich ja indirekt den toLight-Wert übers Dreieck (Ohne das ich dabei aber toLight oder die Normale nochmal normalisiere).