Shadery są już nie odłączanym elementem w grafice 3D i to do tego stopnia, że nie możliwe jest wyświetlanie obrazu bez ustawienia chociaż najprostszego programu cieniującego. Dlatego tym razem opisze pokrótce jak pracować z shaderami na przykładzie oświetlenia i choć te można symulować na kilka różnych sposobów to właśnie shadery będą najłatwiejszym rozwiązaniem na dodanie świtała w naszym wirtualnym świecie. Programu cieniujące, czyli zestawy instrukcji przekazywanych karcie graficznej wymagają znajomości AGAL który jest zupełnie innym językiem od ActionScript i na jego opanowanie potrzebny byłby całkowicie osobny poradnik - tych na szczęście jest całkiem sporo w Internecie, dlatego na razie pominę szczegóły i skupię się tylko na absolutnym minimum potrzebnym do zastosowania prostego cieniowania.
Mając kod z
poprzedniego artykuły, od razu zacznijmy od przygotowania Fragment Shader'a:
private const FRAGMENT_SHADER_LIGHT:String = "dp3 ft1, fc2, v1 n"+
"neg ft1, ft1 n"+
"max ft1, ft1, fc0 n"+
"mul ft2, fc4, ft1 n"+
"mul ft2, ft2, fc3 n"+
"add oc, ft2, fc1";
oraz Vertex Shader'a:
private const VERTEX_SHADER_LIGHT:String = "mov vt0, va0n"+
"m44 op, vt0, vc0n"+
"nrm vt1.xyz, va0.xyzn"+
"mov vt1.w, va0.wn"+
"mov v1, vt1n" +
"mov v2, va1";
(Kod zapożyczony z
http://blog.norbz.net/2012/04/stage3d-agal-from-scratch-part-vii-let-there-be-light)
Powyższe shadery będą rozjaśniać renderowane trójkąty zależnie od ich kąta i ustawienia względem kamery stwarzając wrażenie jakoby ta rzucała światło. Za nim cokolwiek zrobimy z tym kodem musimy go jeszcze skompilować do specjalnego "programu", który przygotujemy wykorzystując
AGALMiniAssembler:
vertexAssembly.assemble(Context3DProgramType.VERTEX, VERTEX_SHADER_LIGHT, false);
fragmentAssembly.assemble(Context3DProgramType.FRAGMENT, FRAGMENT_SHADER_LIGHT, false);
programPairLight = renderContext.createProgram();
programPairLight.upload(vertexAssembly.agalcode, fragmentAssembly.agalcode);
(Nie zapomnij utworzyć zmiennej
programPairLight)
W tym momencie mamy w naszej aplikacji dwa shadery: standardowy
programPair oraz generujący oświetlenie
programPairLight. Pozostanie nam jeszcze tylko zdecydować z którego będziemy korzystać podczas renderowania grafiki 3D wykorzystując funkcje
setProgram w obiekcie
Context3D. Aby łatwiej było widać różnicę w ich działaniu stworzymy osobny obiekt
Box3D (nazwijmy go "box3DLight"), a następnie dopiszemy kod odpowiedzialny za jego wyświetlanie przed wywołaniem
present() ale po wyrysowania "box3D":
boxMat = box3DLight.getMatrix3D();
boxMat.identity();
boxMat.append(rotMat);
boxMat.appendTranslation(posVec.x + 1, posVec.y + 1, posVec.z+1);
finalTransform.identity();
finalTransform.append(boxMat);
finalTransform.append(world);
finalTransform.append(camera);
finalTransform.append(projection);
renderContext.setProgram(programPairLight);
renderContext.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, finalTransform, true);
var p:Vector3D = camera.position.clone();
p.normalize();
var m3d:Matrix3D = finalTransform.clone();
m3d.invert();
p = m3d.transformVector(p);
p.normalize();
p.negate();
renderContext.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, Vector.<Number>([0,0,0,0]));
renderContext.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 1, Vector.<Number>([0,0,0,0]));
renderContext.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 2, Vector.<Number>([p.x,p.y,p.z,1]));
renderContext.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 3, Vector.<Number>([1, 1, 1, 1]));
renderContext.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 4, Vector.<Number>([0.6, 0.6, 0.6, 1]) );
box3DLight.render(renderContext);
Jest tego trochę, ale najważniejsze dla nas to to co się dzieje po linijce
setProgram (operacje na "boxMat" i "finalTransform" to znane nam już transformacje z 3D do 2D). Termin "program" jest tutaj nieprzypadkowy bo shadery podobnie jak każdy inny program na komputerze wykonują pewne operacje matematyczne na zmiennych i stałych, aczkolwiek wykorzystując do tego bardzo nisko-poziomowy język. Dodatkowo w shaderach istnieją duże ograniczenia i tak np. zmienne mogą być tworzone tylko w programie i może ich być góra 8. Na szczęście inaczej sprawa ma się ze stałymi, ponieważ te możemy przekazywać z zewnątrz wykorzystując do tego funkcje
setProgramConstantsFromVector oraz
setProgramConstantsFromMatrix (również maksymalnie 8) i to właśnie dzięki nim do programu cieniującego przekażemy informacje o położeniu światła (zmienna "p") i jego kolorze.
Jeśli wszystko pójdzie zgodnie z planem powinniśmy zobaczyć coś takiego:
W tym przykładzie operujemy jedynie na dwóch shaderach, jednak nic nie stoi na przeszkodzie by stworzyć ich o wiele, wiele więcej a następnie wymieniać je funkcją
setProgram gdy tylko chcemy, podobnie jak w
Graphics wywołujemy funkcję
lineStyle.
Całość do pobrania stąd:
Tutorial3D_light.zip.