som nævnt i Hello Triangle kapitlet er shaders små programmer, der hviler på GPU ‘ en. Disse programmer køres for hvert specifikt afsnit af grafikrørledningen. I en grundlæggende forstand er shaders intet andet end programmer, der omdanner input til output. Shaders er også meget isolerede programmer, idet de ikke har lov til at kommunikere med hinanden; den eneste kommunikation, de har, er via deres input og output.
i det foregående kapitel berørte vi kort overfladen af shaders og hvordan man korrekt bruger dem. Vi vil nu forklare shaders, og specifikt OpenGL-Skyggesproget, på en mere generel måde.
GLSL
Shaders er skrevet på det C-lignende sprog GLSL. GLSL er skræddersyet til brug med grafik og indeholder nyttige funktioner, der specifikt er målrettet mod vektor-og matricsmanipulation.
Shaders begynder altid med en versionserklæring efterfulgt af en liste over input-og outputvariabler, uniformer og dens hovedfunktion. Hver Skyggers indgangspunkt er ved sin hovedfunktion, hvor vi behandler alle inputvariabler og udsender resultaterne i dens outputvariabler. Bare rolig, hvis du ikke ved, hvad uniformer er, vi kommer snart til dem.
en shader har typisk følgende struktur:
#version version_numberin type in_variable_name;in type in_variable_name;out type out_variable_name; uniform type uniform_name; void main(){ // process input(s) and do some weird graphics stuff ... // output processed stuff to output variable out_variable_name = weird_stuff_we_processed;}
Når vi taler specifikt om toppunktets shader, er hver inputvariabel også kendt som en toppunktsattribut. Der er et maksimalt antal toppunktattributter, vi har lov til at erklære begrænset af udstyret. OpenGL garanterer, at der altid er mindst 16 4-komponent toppunktattributter tilgængelige, men noget udstyr kan muligvis give mulighed for mere, som du kan hente ved at forespørge:
int nrAttributes;glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
dette returnerer ofte minimum16
som skal være mere end nok til de fleste formål.
typer
GLSL har ligesom alle andre programmeringssprog datatyper til at specificere, hvilken type variabel vi vil arbejde med. GLSL har de fleste af de standard grundlæggende typer, vi kender fra sprog som C: int
float
double
uint
og bool
. GLSL har også to containertyper, som vi bruger meget, nemlig vectors
og matrices
. Vi vil diskutere matricer i et senere kapitel.
vektorer
en vektor i GLSL er en 1,2,3 eller 4 komponentbeholder til nogen af de netop nævnte basistyper. De kan tage følgende form (n
repræsenterer antallet af komponenter):
-
vecn
: standardvektoren afn
flyder. -
bvecn
: en vektor afn
booleans. -
ivecn
: en vektor afn
heltal. -
uvecn
: en vektor afn
usignerede heltal. -
dvecn
: en vektor afn
dobbelt komponenter.
det meste af tiden bruger vi det grundlæggendevecn
da flyder er tilstrækkelige til de fleste af vores formål.
komponenter i en vektor kan fås via vec.x
hvor x
er den første komponent i vektoren. Du kan bruge .x
.y
.z
og .w
for at få adgang til henholdsvis deres første, anden, tredje og fjerde komponent. GLSL giver dig også mulighed for at bruge rgba
til farver eller stpq
til teksturkoordinater, adgang til de samme komponenter.
vektordatatypen giver mulighed for et interessant og fleksibelt komponentvalg kaldet svingende. Svirrende giver os mulighed for at bruge syntaks som denne:
vec2 someVec;vec4 differentVec = someVec.xyxx;vec3 anotherVec = differentVec.zyw;vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
Du kan bruge en hvilken som helst kombination af op til 4 bogstaver til at oprette en ny vektor (af samme type), så længe den originale vektor har disse komponenter; det er ikke tilladt at få adgang til.z
komponent af envec2
for eksempel. Vi kan også videregive vektorer som argumenter til forskellige vektorkonstruktøropkald, hvilket reducerer antallet af krævede argumenter:
vec2 vect = vec2(0.5, 0.7);vec4 result = vec4(vect, 0.0, 0.0);vec4 otherResult = vec4(result.xyz, 1.0);
vektorer er således en fleksibel datatype, som vi kan bruge til alle former for input og output. I hele bogen kan du se masser af eksempler på, hvordan vi kreativt kan styre vektorer.
Ins and outs
Shaders er dejlige små programmer alene, men de er en del af en helhed, og derfor ønsker vi at have input og output på de enkelte shaders, så vi kan flytte ting rundt. GLSL defineredein
ogout
nøgleord specifikt til dette formål. Hver shader kan angive indgange og udgange ved hjælp af disse søgeord, og hvor en outputvariabel matcher med en inputvariabel i det næste shader-trin, sendes de videre. Toppen og fragmentet shader adskiller sig dog lidt.
toppunktets skygge skal modtage en form for input, ellers ville det være ret ineffektivt. Toppunktsskyggen adskiller sig i dens input, idet den modtager sin input lige fra toppunktsdataene. For at definere, hvordan toppunktdataene er organiseret, specificerer vi inputvariablerne med placeringsmetadata, så vi kan konfigurere toppunktattributterne på CPU ‘ en. Vi har set dette i det foregående kapitel som layout (location = 0)
. Toppunktsskyggen kræver således en ekstra layoutspecifikation for dens input, så vi kan forbinde den med toppunktsdataene.
det er også muligt at udeladelayout (location = 0)
specifier og forespørgsel for attributplaceringerne i din OpenGL-kode via glGetAttribLocation, men jeg foretrækker at indstille dem i toppunktet shader. Det er lettere at forstå og sparer dig (og OpenGL) noget arbejde.
den anden undtagelse er, at fragment shader kræver en vec4
farveudgangsvariabel, da fragment shaders skal generere en endelig Outputfarve. Hvis du undlader at angive en Outputfarve i din fragment shader, vil farvebufferudgangen for disse fragmenter være udefineret (hvilket normalt betyder, at OpenGL vil gøre dem enten sort eller hvid).
så hvis vi ønsker at sende data fra den ene shader til den anden, bliver vi nødt til at erklære en output i den afsendende shader og en lignende input i den modtagende shader. Når typerne og navnene er ens på begge sider, vil OpenGL forbinde disse variabler sammen, og så er det muligt at sende data mellem shaders (dette gøres ved sammenkædning af et programobjekt). For at vise dig, hvordan dette fungerer i praksis, vil vi ændre shaders fra det forrige kapitel for at lade toppunktets skygge bestemme farven for fragmentskyggen.
toppunkt shader
#version 330 corelayout (location = 0) in vec3 aPos; // the position variable has attribute position 0 out vec4 vertexColor; // specify a color output to the fragment shadervoid main(){ gl_Position = vec4(aPos, 1.0); // see how we directly give a vec3 to vec4's constructor vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // set the output variable to a dark-red color}
Fragment shader
#version 330 coreout vec4 FragColor; in vec4 vertexColor; // the input variable from the vertex shader (same name and same type) void main(){ FragColor = vertexColor;}
Du kan se, at vi erklærede en vertekspolorvariabel som envec4
output, som vi indstiller i toppunktet shader, og vi erklærer en lignende vertekspolor input i fragmentet shader. Da de begge har samme type og navn, er vertekspoloren i fragmentskyggeren knyttet til vertekspoloren i toppunktskyggeren. Fordi vi indstiller farven til en mørkerød farve i toppunktets skygge, skal de resulterende fragmenter også være mørkerøde. Følgende billede viser output:
der går vi! Vi har lige formået at sende en værdi fra toppunktet shader til fragment shader. Lad os krydre det lidt og se, om vi kan sende en farve fra vores applikation til fragmentet shader!
uniformer
uniformer er en anden måde at videregive data fra vores applikation på CPU ‘en til shaders på GPU’ en. Uniformer er dog lidt anderledes i forhold til toppunkt attributter. For det første er uniformer globale. Global, hvilket betyder, at en ensartet variabel er unik pr shader program objekt, og kan tilgås fra enhver shader på ethvert tidspunkt i shader program. For det andet, uanset hvad du indstiller den ensartede værdi til, vil uniformer beholde deres værdier, indtil de enten nulstilles eller opdateres.
for at erklære en uniform i GLSL tilføjer vi blot uniform
søgeord til en shader med en type og et navn. Fra det tidspunkt kan vi bruge den nyligt erklærede uniform i skyggen. Lad os se, om vi denne gang kan indstille farven på trekanten via en uniform:
#version 330 coreout vec4 FragColor; uniform vec4 ourColor; // we set this variable in the OpenGL code.void main(){ FragColor = ourColor;}
vi erklærede en ensartetvec4
ourColor i fragment shader og indstil fragmentets Outputfarve til indholdet af denne ensartede værdi. Da uniformer er globale variabler, kan vi definere dem i ethvert skyggefase, vi gerne vil have, så det er ikke nødvendigt at gå gennem toppunktets skygge igen for at få noget til fragmentskyggen. Vi bruger ikke denne uniform i toppunktet shader, så der er ingen grund til at definere det der.
Hvis du erklærer en uniform, der ikke bruges overalt i din GLSL-kode, fjerner kompilatoren lydløst variablen fra den kompilerede version, hvilket er årsagen til flere frustrerende fejl; husk det!
uniformen er i øjeblikket tom; vi har ikke tilføjet nogen data til uniformen endnu, så lad os prøve det. Vi skal først finde indekset / placeringen af den ensartede attribut i vores shader. Når vi har indekset/placeringen af uniformen, kan vi opdatere dens værdier. I stedet for at overføre en enkelt farve til fragmentet shader, lad os krydre tingene ved gradvist at ændre farve over tid:
float timeValue = glfwGetTime();float greenValue = (sin(timeValue) / 2.0f) + 0.5f;int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");glUseProgram(shaderProgram);glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
først henter vi køretiden i sekunder via glfgettime(). Derefter varierer vi farven i området 0.0
1.0
ved at bruge sin-funktionen og gemme resultatet i greenValue.
så spørger vi efter placeringen af ourColor-uniformen ved hjælp af glGetUniformLocation. Vi leverer shader-programmet og navnet på uniformen (som vi vil hente placeringen fra) til forespørgselsfunktionen. Hvis glGetUniformLocation returnerer -1
, kunne den ikke finde placeringen. Endelig kan vi indstille den ensartede værdi ved hjælp af glUniform4f-funktionen. Bemærk, at det at finde den ensartede placering ikke kræver, at du først bruger shader-programmet, men opdatering af en uniform kræver, at du først bruger programmet (ved at ringe til glUseProgram), fordi det indstiller uniformen på det aktuelt aktive shader-program.
fordi OpenGL i sin kerne er et c-bibliotek, har det ikke indbygget understøttelse af overbelastning af funktioner, så uanset hvor en funktion kan kaldes med forskellige typer, definerer OpenGL nye funktioner for hver type, der kræves; glUniform er et perfekt eksempel på dette. Funktionen kræver en specifik postrettelse for den type uniform, du vil indstille. Et par af de mulige postrettelser er:
-
f
: funktionen forventer enfloat
som dens værdi. -
i
: funktionen forventer enint
som dens værdi. -
ui
: funktionen forventer enunsigned int
som dens værdi. -
3f
: funktionen forventer 3float
s som dens værdi. -
fv
: funktionen forventer enfloat
vektor/array som dens værdi.
når du vil konfigurere en mulighed for OpenGL, skal du blot vælge den overbelastede funktion, der svarer til din type. I vores tilfælde ønsker vi at indstille 4 floats af uniformen individuelt, så vi sender vores data via glUniform4f (bemærk at vi også kunne have brugtfv
version).
nu hvor vi ved, hvordan vi indstiller værdierne for ensartede variabler, kan vi bruge dem til gengivelse. Hvis vi ønsker, at farven gradvist ændres, vil vi opdatere denne ensartede hver ramme, ellers ville trekanten opretholde en enkelt ensfarvet farve, hvis vi kun indstiller den en gang. Så vi beregner greenValue og opdaterer den ensartede hver gengivelses iteration:
while(!glfwWindowShouldClose(window)){ // input processInput(window); // render // clear the colorbuffer glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // be sure to activate the shader glUseProgram(shaderProgram); // update the uniform color float timeValue = glfwGetTime(); float greenValue = sin(timeValue) / 2.0f + 0.5f; int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // now render the triangle glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); // swap buffers and poll IO events glfwSwapBuffers(window); glfwPollEvents();}
koden er en relativt ligetil tilpasning af den foregående kode. Denne gang opdaterer vi en ensartet værdi hver ramme, før vi tegner trekanten. Hvis du opdaterer uniformen korrekt, skal du se farven på din trekant gradvist skifte fra grøn til sort og tilbage til grøn.
Tjek kildekoden her, hvis du sidder fast.
som du kan se, er uniformer et nyttigt værktøj til at indstille attributter, der kan ændre hver ramme, eller til at udveksle data mellem din applikation og dine shaders, men hvad nu hvis vi vil indstille en farve for hvert toppunkt? I så fald skal vi erklære så mange uniformer som vi har hjørner. En bedre løsning ville være at inkludere flere data i toppunktets attributter, hvilket er hvad vi skal gøre nu.
flere attributter!
vi så i det forrige kapitel, hvordan vi kan udfylde en VBO, konfigurere toppunktattributpegere og gemme det hele i en VAO. Denne gang vil vi også tilføje farvedata til toppunktdataene. Vi vil tilføje farvedata som 3float
s til vertices array. Vi tildeler en rød, grøn og blå farve til hvert af hjørnerne af vores trekant henholdsvis:
float vertices = { // positions // colors 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top };
da vi nu har flere data at sende til toppunktsskyggen, er det nødvendigt at justere toppunktsskyggen for også at modtage vores farveværdi som en toppunktsattributindgang. Bemærk, at vi indstiller placeringen af aColor-attributten til 1 med layoutspecifikatoren:
#version 330 corelayout (location = 0) in vec3 aPos; // the position variable has attribute position 0layout (location = 1) in vec3 aColor; // the color variable has attribute position 1 out vec3 ourColor; // output a color to the fragment shadervoid main(){ gl_Position = vec4(aPos, 1.0); ourColor = aColor; // set ourColor to the input color we got from the vertex data}
da vi ikke længere bruger en uniform til fragmentets farve, men nu bruger ourcolor-outputvariablen, bliver vi også nødt til at ændre fragmentskyggen:
#version 330 coreout vec4 FragColor; in vec3 ourColor; void main(){ FragColor = vec4(ourColor, 1.0);}
fordi vi tilføjede en anden toppunktattribut og opdaterede VBO ‘s shader:
#version 330 coreout vec4 FragColor; in vec3 ourColor; void main(){ FragColor = vec4(ourColor, 1.0);}
fordi vi tilføjede en anden toppunktattribut og opdaterede hukommelse vi er nødt til at konfigurere toppunktattributpegerne igen. De opdaterede data i VBO ‘ s hukommelse ser nu lidt sådan ud:
Når vi kender det aktuelle layout, kan vi opdatere toppunktformatet med glverteksattribpointer:
// position attributeglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);// color attributeglVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));glEnableVertexAttribArray(1);
de første par argumenter for glverteksattribpointer er relativt ligetil. Denne gang konfigurerer vi toppunktattributten på attributplacering 1
. Farveværdierne har en størrelse på 3
float
s, og vi normaliserer ikke værdierne.
da vi nu har to toppunktattributter, er vi nødt til at beregne stride-værdien igen. For at få den næste attributværdi (f.eks. den næste x
komponent af positionsvektoren) i datarrayet skal vi flytte 6
float
s til højre, tre for positionsværdierne og tre for farveværdierne. Dette giver os en skridtværdi på 6 gange størrelsen af en float
i bytes (= 24
bytes).
også denne gang skal vi angive en forskydning. For hvert toppunkt er attributten position toppunkt først, så vi erklærer en forskydning af 0
. Farveattributten starter efter positionsdataene, så forskydningen er 3 * sizeof(float)
i bytes (= 12
bytes).
kørsel af applikationen skal resultere i følgende billede:
Tjek kildekoden her, hvis du sidder fast.
billedet er muligvis ikke nøjagtigt, hvad du ville forvente, da vi kun leverede 3 farver, ikke den enorme farvepalet, vi ser lige nu. Dette er alt resultatet af noget, der kaldes fragmentinterpolation i fragmentet shader. Ved gengivelse af en trekant resulterer rasteriseringstrinnet normalt i meget flere fragmenter end hjørner oprindeligt specificeret. Rasterisatoren bestemmer derefter positionerne for hvert af disse fragmenter baseret på, hvor de befinder sig på trekantformen.
baseret på disse positioner interpolerer det alle fragment shader ‘ s inputvariabler. Sig for eksempel, at vi har en linje, hvor det øverste punkt har en grøn farve og det nederste punkt en blå farve. Hvis fragment shader køres på et fragment, der ligger omkring en position ved 70%
af linjen, ville dens resulterende farveindgangsattribut derefter være en lineær kombination af grøn og blå; for at være mere præcis: 30%
blå og 70%
grøn.
dette er præcis, hvad der skete i trekanten. Vi har 3 hjørner og dermed 3 farver, og ud fra trekantens billedpunkter indeholder den sandsynligvis omkring 50000 fragmenter, hvor fragmentet shader interpolerede farverne blandt disse billedpunkter. Hvis du tager et godt kig på de farver, du vil se det hele giver mening: rød til blå først får til lilla og derefter til blå. Fragment interpolation anvendes på alle fragment shader input attributter.
vores egen shader klasse
skrivning, kompilering og styring af shaders kan være ret besværligt. Som et sidste strejf på shader-emnet vil vi gøre vores liv lidt lettere ved at opbygge en shader-klasse, der læser shaders fra disken, samler og forbinder dem, kontrollerer for fejl og er nem at bruge. Dette giver dig også en ide om, hvordan vi kan indkapsle noget af den viden, vi har lært hidtil, til nyttige abstrakte objekter.
Vi opretter shader-klassen helt i en headerfil, hovedsageligt til læringsformål og bærbarhed. Lad os starte med at tilføje de krævede inkluderer og ved at definere klassestrukturen:
#ifndef SHADER_H#define SHADER_H#include <glad/glad.h> // include glad to get all the required OpenGL headers #include <string>#include <fstream>#include <sstream>#include <iostream> class Shader{public: // the program ID unsigned int ID; // constructor reads and builds the shader Shader(const char* vertexPath, const char* fragmentPath); // use/activate the shader void use(); // utility uniform functions void setBool(const std::string &name, bool value) const; void setInt(const std::string &name, int value) const; void setFloat(const std::string &name, float value) const;}; #endif
vi brugte flere præprocessordirektiver øverst i overskriftsfilen. Brug af disse små kodelinjer informerer din kompilator om kun at inkludere og kompilere denne header-fil, hvis den ikke er inkluderet endnu, selvom flere filer inkluderer shader-overskriften. Dette forhindrer sammenkædning af konflikter.
shader-klassen har id ‘ et for shader-programmet. Dens konstruktør kræver filstierne i kildekoden til henholdsvis toppunktet og fragmentet shader, som vi kan gemme på disken som enkle tekstfiler. For at tilføje lidt ekstra tilføjer vi også flere hjælpefunktioner for at lette vores liv lidt: brug aktiverer shader-programmet og alt indstillet… funktioner Forespørg på en ensartet placering og indstil dens værdi.
læsning fra fil
Vi bruger C++ filestreams til at læse indholdet fra filen i flerestring
objekter:
Shader(const char* vertexPath, const char* fragmentPath){ // 1. retrieve the vertex/fragment source code from filePath std::string vertexCode; std::string fragmentCode; std::ifstream vShaderFile; std::ifstream fShaderFile; // ensure ifstream objects can throw exceptions: vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); try { // open files vShaderFile.open(vertexPath); fShaderFile.open(fragmentPath); std::stringstream vShaderStream, fShaderStream; // read file's buffer contents into streams vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); // close file handlers vShaderFile.close(); fShaderFile.close(); // convert stream into string vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); } catch(std::ifstream::failure e) { std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl; } const char* vShaderCode = vertexCode.c_str(); const char* fShaderCode = fragmentCode.c_str();
næste skal vi kompilere og linke shaders. Bemærk, at vi også gennemgår, om kompilering/sammenkædning mislykkedes, og i så fald udskriver kompileringstidsfejlene. Dette er yderst nyttigt, når debugging (du skal bruge disse fejllogfiler til sidst):
// 2. compile shadersunsigned int vertex, fragment;int success;char infoLog; // vertex Shadervertex = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertex, 1, &vShaderCode, NULL);glCompileShader(vertex);// print compile errors if anyglGetShaderiv(vertex, GL_COMPILE_STATUS, &success);if(!success){ glGetShaderInfoLog(vertex, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;}; // similiar for Fragment Shader // shader ProgramID = glCreateProgram();glAttachShader(ID, vertex);glAttachShader(ID, fragment);glLinkProgram(ID);// print linking errors if anyglGetProgramiv(ID, GL_LINK_STATUS, &success);if(!success){ glGetProgramInfoLog(ID, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;} // delete the shaders as they're linked into our program now and no longer necessaryglDeleteShader(vertex);glDeleteShader(fragment);
brugsfunktionen er ligetil:
void use() { glUseProgram(ID);}
tilsvarende for nogen af de ensartede setter funktioner:
void setBool(const std::string &name, bool value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); }void setInt(const std::string &name, int value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), value); }void setFloat(const std::string &name, float value) const{ glUniform1f(glGetUniformLocation(ID, name.c_str()), value); }
og der har vi det, en afsluttet shader klasse. Brug af shader-klassen er ret let; vi opretter et shader-objekt en gang og fra det tidspunkt begynder vi blot at bruge det:
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");while(...){ ourShader.use(); ourShader.setFloat("someUniform", 1.0f); DrawStuff();}
Her lagrede vi toppunktet og fragmentet shader-kildekoden i to filer kaldet shader.vs
og shader.fs
. Du er fri til at navngive dine shader-filer, som du vil; Jeg finder personligt udvidelserne .vs
og .fs
ganske intuitivt.
Du kan finde kildekoden her ved hjælp af vores nyoprettede shader klasse. Bemærk, at du kan klikke på shader-filstierne for at finde shaders kildekode.
øvelser
- Juster toppunktets skygge, så trekanten er på hovedet: løsning.
- Angiv en vandret forskydning via en uniform, og flyt trekanten til højre side af skærmen i toppunktets skygge ved hjælp af denne forskydningsværdi: løsning.
- send toppunktpositionen til fragmentskyggeren ved hjælp af nøgleordet
out
og indstil fragmentets farve svarende til denne toppunktposition (se, hvordan selv toppunktspositionsværdierne interpoleres over trekanten). Når det lykkedes dig at gøre dette; prøv at besvare følgende spørgsmål: Hvorfor er den nederste venstre side af vores trekant sort?: løsning.