Çizimin içinde WebGL
Bir shader efektini (ateş, su, ışık, parlama) izometrik bir şeklin içine, aynı kameradan görülen aynı nesnenin bir parçası olarak okunacak şekilde nasıl koyacağınız. Bunu yapan iki yayınlanmış örnek vardır: examples/raptor-engine/ (bir roket motorunun altında bir alev, şok dalgası, sıçrama ve buhar) ve examples/ripple-tank/ (kostikler, gölgeler ve dalgalı bir su yüzeyi). Bu sayfanın yanında onların live.js'ini okuyun.
Kullanıcı ilk shader şeklini çok sevdi, ama rengin evreleri arasında, efekt kesilmiş gibi anında değiştiğini fark etti. Bu yüzden çıta şudur: hiçbir şey patlamaz, hiçbir şey kesilmez ve her şey çizimin perspektifinde durur. Aşağıdaki her kural bu çıtayı kıran bir hatadan geldi.
İçindekiler
- Bir shader ne zaman yerini hak eder
- Katman yığını
- Kamera sözleşmesi
- İzometrik kamerada ışın yürütme
- Shader içeriğini SVG'ye hizalamak
- API ve minimal bir sayfa
- Her biri onu öğreten hatayla zanaat kuralları
- Açık tema (8b. Dönen bir gruptaki shader'lar)
- Doğrulama
- Performans
1. Bir shader ne zaman yerini hak eder
Çizgilerin çizemeyeceği şeyler için bir shader kullanın: yayılan ışık ve parlama, ateş ve egzoz, su yüzeyleri, kostikler, gerçek bir lambadan yumuşak gölgeler, duman ve buhar. Bunlar sürekli alanlardır ve konunun kendisi ya da konunun yaptığı şeydir.
Şunlar için kullanmayın:
- Katılar. Parçalar, kenarlar, çentikler ve vidalar kitten gelen SVG olarak kalır. Shader asla SVG'nin çizebileceği bir parçayı çizmez.
- Süsleme. Işıltı, arka plan gradyanları, her kenarda bloom ya da şekli "zengin" göstermek için eklenen gürültü. Efekti silebiliyorsanız ve açıklama hâlâ geçerliyse, silin.
- SVG'nin zaten iyi gösterdiği bir şey. Parlayan bir LED bir shader değil,
litbir noktadır.
SVG kendi başına da okunmalıdır. WebGL'siz Raptor motoru bir kenar ışığı ve bir CSS platform parlamasıyla hâlâ gösterir. Dalga leğeni tepelerini ve düğüm çizgilerini SVG çizgileri olarak çizer (model.mjs'teki patternOf), kâğıttaki leğen gölgesinin çizilen dış hattına kırpılmış olarak, ve WebGL çalıştığında onları .rt-stage[data-gl] .rt-fallback { display: none } ile gizler.
2. Katman yığını
Bir shader SVG'ye karşı derinlik testine sokulamaz; bu yüzden onun yerine katmanları sıralarsınız. Temel yığın, hepsi aynı viewBox'a sahip, tek bir position: relative sahnedeki üç katmandır:
back SVG efektin örttüğü şey (k.figureSvg: role="img" ve etiket)
canvas efekt (position:absolute; inset:0; pointer-events:none)
front SVG efekti örten şey (aria-hidden)
Her parça için tek bir soru sorun: efekt tarafından hiç örtülüyor mu, yoksa efekti mi örtüyor? Örtülen parçalar arka SVG'ye, örten parçalar ön SVG'ye gider. Bir parça ikisini birden yapıyorsa üç seçeneğiniz vardır:
- Sanatı bölün. Parçayı parçalara kesin ve her parçayı ait olduğu katmana ve o katmandaki yuvaya gönderin. Dalga leğeninin motor kablosu üç
tubePiecesgrubudur: köprünün üzerindeki uzunluk ön SVG'de çubuktan önce, yakın kenar boyunca uzunluk ön SVG'de yakın kapaklardan sonra ve sol ayaktan kontrol kutusuna inen uzunluk orta SVG'de. - Onu shader'da modelleyin. Parça basit bir şekilse (kutu, silindir, torus) ve arka SVG'de kalıyorsa, shader'a o şekli bir örtücü olarak verin. Raptor'un kolonları opak kutulardır (
columnNear); bu yüzden her hava integralimax(tGround, tColumn)'da başlar. Su püskürtme halkasıgroundHitiçinde bir torustur; böylece sıçrama iç tarafını aydınlatır ve dış tarafını karanlık bırakır. - Efekti bölün. Efekt ön SVG parçalarının önünden geçtiğinde, ön SVG'nin üstüne ikinci bir tuval ekleyin. Örten parçanın analitik bir vekili her örnek için arka tuvale (ağırlık
1 − f) mi yoksa ön tuvale (ağırlıkf) mi gideceğine karar verir. Her örnek tam olarak bir kez çizilir; bu yüzden dikiş olmaz. Vekili cömert yapın: gerçek siluetin dışında iki katman da aynı görünür.
Çözümlü örnek: Raptor
back SVG platform, ayaklar, alev konisi, su püskürtme halkası, kolonlar, merdiven, kablo kanalı
canvas .rp-plume: alev hacmi, şok (arka yarı), parlama, sıçrama, zemin halkası, motorun arkasındaki buhar
front SVG her motor öğesi (derinliğe göre sıralanmış .it grupları), borular, kablo demeti, travers
glow SVG .rp-glow: iki lüle dudağı kenarı, yalnızca ışık ekleyebilsinler diye screen ile karıştırılmış
canvas .rp-veil: motorun önünde kalan buhar ve şok örnekleri
div .rp-flash: ateşleme parlaması için bir CSS radyal gradyanı (closest-side)
Motor vekili engineR(z)'dir; çanın, yanma odasının ve pompaların bir yarıçap fonksiyonu. inFront(p, V), p örneğinin motor ile izleyici arasında olup olmadığını söyler. side(front, veil) örneği bir tuvale ya da diğerine gönderir.
Çözümlü örnek: dalga leğeni
back SVG taban plakası, ayaklar, cetvel, etiket plakası, bantlı kâğıt, taban kenarı üzerinden lamba kablosu, SVG yedek deseni
canvas .rt-paper-gl: kâğıt ve taban üzerinde lamba ışığı, kostikler, ayakların, direğin, köprünün, motorun, kontrol kutusunun ve köpük kıyının gölgeleri, kırılma
middle SVG direk, kol, lamba ve kabloları, kontrol kutusu, klipslerde sol ayaktan inen motor kablosu, ayar vidalı ayaklar,
uzak raylar, üç köşe direği, uzak cam, köpük kıyı, düz su dolgusu
canvas .rt-water-gl: pencere yansıması ve oda ışığı parıltıları, duvarlarda menisküs eğimi, daldırıcı temas halkaları,
yakın camın arkasındaki su bandı, cam tonu, lambanın ışını (yalnızca koyu)
front SVG uzak kapaklar, köprü üzerinden motor kablosu, uzak sütun ve yay, çubuk, daldırıcılar, motor, yakın yay, yakın raylar,
cam taban, su çizgileri, yakın cam, yakın kapaklar, kenardaki motor kablosu, yakın köşe direği, yakın sütun, ışın
İki tuval iki derinlikte durur, çünkü iki yükseklikte iki efekt vardır: kâğıt üzerindeki ışık (ayakların altında) ve su yüzeyi (leğenin içinde). Kontrol kutusu leğenin yanında tabanın üzerinde, su tuvalinin altında durur; bu yüzden orta SVG'de yaşar; yakın köşe direği üzerinde durduğu kapaklardan sonra boyanır.
Yığın için CSS (ripple.css'ten uyarlanmış):
.fx-stage { position: relative; touch-action: pan-y; }
.fx-stage > svg, .fx-stage > canvas { display: block; width: 100%; height: auto; }
.fx-stage > .fx-layer { position: absolute; inset: 0; height: 100% !important; pointer-events: none; }
.iso:not([data-theme="light"]) .fx-gl { mix-blend-mode: screen; }
Screen karışımı yalnızca aydınlatabilir. Örttüğünü koyulaştırması da gereken bir tuval (içinden görülen kâğıdı renklendiren dalga leğeninin suyu) koyu temada da normal önceden çarpılmış karışımda kalır: yalnızca .rt-paper-gl screen ile karıştırılır.
Ön SVG'nin üzerine gelmeyi alması gerekiyorsa (Raptor'un parçaları alır), ona pointer-events: none ve öğelerine pointer-events: visiblePainted verin; böylece tuval ve boş alanlar işaretçiyi asla yakalamaz.
3. Kamera sözleşmesi
kit/gl.mjs, parçanızın başına GLSL_ISO'yu ekler. Bu, shader'ın iso() ile aynı kamerayı görmesini sağlayan uniform'ları ve fonksiyonları bildirir:
| Ad | Ne olduğu |
|---|---|
uCanvas |
Aygıt piksellerinde tuval boyutu. |
uView |
viewBox boyutu [W, H]. |
uOrigin, uK |
İzdüşüm orijini ve dünya birimi başına ekran birimi (glCamera(P).k, scaleOf(P)'ye eşit). |
uCam |
Azimut ve yüksekliğin (sinA, cosA, sinE, cosE)'si. |
uTime |
draw'a verdiğiniz zaman. |
viewOf(gl_FragCoord.xy) |
Aygıt pikselinden viewBox noktasına (y'yi çevirir). |
project(vec3 p) |
Dünyadan viewBox'a, kitin iso()'suyla özdeş. |
towardViewer() |
Birim 3B görüş yönü, (cosA·cosE, sinA·cosE, sinE). Her p + t·towardViewer() aynı piksele düşer. 2B zemin yönünü döndüren kitin JS towardViewer(P)'siyle aynı değildir. |
rayOf(vb) |
Orijinden geçen ve kameraya bakan dikey düzlemde vb'nin altındaki dünya noktası; böylece rayOf(vb) + t·towardViewer() pikselin ışınıdır. |
pixelOf() |
Ekran boyunca tuval pikseli başına dünya birimi, (uView.x / uCanvas.x) / uK. |
lightDir() |
Kitin ışığı, lightOf(P)'ye eşit; böylece shader gölgelendirmesi SVG tonlarıyla uyuşur. |
onFloor(vb, z) |
Bu pikselin altındaki, z yüksekliğindeki yatay düzlem üzerindeki dünya noktası. |
onWallX(vb, x), onWallY(vb, y) |
Bu pikselin altındaki x = sabit ya da y = sabit dikey düzlemindeki dünya noktası. |
hash12, hash13, noise2, noise3, fbm2, fbm3 |
Hash'ler ve değer gürültüsü, artı 4 oktavlı fbm. |
window(x, a, b, soft) |
[a + soft, b − soft] içinde 1 ve [a − soft, b + soft] dışında 0: her sınırda ortalanmış 2·soft genişliğinde bir smoothstep; yani a ve b'de 0.5. |
tonemap(c, exposure) |
1 − exp(−c·exposure). clamp yerine kullanın. |
onFloor ve duvar fonksiyonları iso() ile tam gidiş-dönüş yapar: project(onFloor(vb, z)) == vb. Shader içeriğinin SVG çizgileriyle hizalanmasını sağlayan budur.
Neden dünya uzayı. Şeklin perspektifi izdüşümdür. Bir efekti ekran uzayında çizerseniz (gözle boyutlandırılmış bir elips, piksellerde bir parlama sütunu), çizimle uyuşamaz. Zeminin üstünde süzülür, platformun içinden geçer ya da kamera değiştiğinde aynı şekli korur. Her şeyi dünya noktalarından ve dünya boyutlarından kurun. Ekran boyutları yalnızca dünya birimleri çarpı uK'den gelir.
Dünya birimlerinde kenar yumuşatma: pixelOf(), ekran boyunca aygıt pikseli başına dünya birimidir. Derinlik yönünde zeminde onu uCam.z'ye (sinE), bir duvarda yukarı uCam.w'ye (cosE) bölün. Hizalanmış kenarları yaklaşık 0.7 * px yumuşatın; böylece bir piksel yumuşak çıkarlar: SVG gibi keskin ama tırtıklı değil. Kırılmanın görüntüyü gerdiği yerde sabit bir yumuşaklık merdiven yapar; dalga leğeni GL_OES_standard_derivatives'i etkinleştirir ve gölge yarı gölgesini length(fwidth(q.xy)) / length(fwidth(rest)) ile, yani kaydırılmış noktanın izinin kaydırılmamışınkine oranıyla genişletir; bu dinlenmede tam olarak 1'dir; bir #ifdef GL_OES_standard_derivatives yedeğiyle.
4. İzometrik kamerada ışın yürütme
Kamera tekdüze ölçekli ortografiktir: izdüşümün iki satırı da ortonormaldir. Üç şey çıkar.
- Her piksel düz bir ışındır
p(t) = p0 + t·V; buradaV = towardViewer()vep0pikselin altındaki herhangi bir dünya noktasıdır.p0 = onFloor(vb, h)ya da kitinrayOf(vb)'sini, yani orijinden geçen ve kameraya bakan dikey düzlemdeki noktayı alın (Raptor'un ışın durumu orada başlar:S.g0 = rayOf(vb)). Daha büyüktizleyiciye daha yakındır. - Noktalara uzaklıklar ekran uzaklıklarıdır. Ekran uzaklığı bölü
uK, pikselin ışınından bir dünya noktasına olan tam dik uzaklıktır. Küresel bir parlama, şok kabuğu ya da parlama topu bu uzaklıktan hesaplanabilir ve yine dünyaya göre doğru olur. En yakın yaklaşma parametresitc = dot(C − p0, V)'dir. - Bir örtücünün arkasındaki görünür oran. Işın boyunca ilk opak yüzey
tLow'daysa,tc'de ortalanmışsigmagenişliğinde bir Gauss1/(1 + exp(2.41·(tLow − tc)/sigma))kadar görünür (Raptor'unbeyond'u). Bu yumuşak ama geometrik bir kesim verir.
Bir hacmi yürütmek için:
- Işın boyunca ilk opak isabeti bulun: zemin, artı SVG'nin çizdiği herhangi bir yükseklik alanı. Raptor alev konisinin ucunun üstünden platforma kadar 28 adım atar ve sonra SVG torna profiliyle aynı sayılardan kurulmuş bir
ground(r)yükseklik alanında 7 kez ikiye böler. Işının yalnızca isabetin önündeki kısmı (t > tLow) havadır. - Yürütmeyi basit bir şekille sınırlayın (Raptor zemin isabetinden çıkış düzlemine kadar 132 yarıçaplı bir silindir kullanır). Her terimi o sınırdan önce tam olarak sıfıra pencereleyin; böylece sınır yalnızca bir optimizasyondur.
- Titreşimle örnekleyin. 28 örnek bantlaşmasın diye
t = ta + (i + hash12(gl_FragCoord.xy))·dtkullanın. - Her terimi kirişine göre normalleştirin; böylece merkez piksel adım sayısı ne olursa olsun amaçlanan parlaklığı alır. Dikey eksenli bir hacim için
r² = u² + (t·cosE)²'dir; bu yüzdenσyarıçaplı bir Gauss ışın boyuncaσ√π / cosE'ye integrallenir: her örneğidt·cosE/(σ·1.772)ile ağırlıklandırın. - Tekdüze bir silindirin daha yumuşak görünmesini bekleyin. İzdüşürülmüş profili karekök biçimindedir; bu yüzden düz bir çizimin sahip olduğu belirgin kenarı kaybeder. Koyu temada kenarı geri getirmek için bir kenar kabuğu ekleyin (Raptor'un
0.9·Re'deki turuncu kesme katmanı). Açık temada aynı kabuk içi boş bir alevin çevresine bir dış hat mürekkepler; bu yüzden orada gövdeyi eksene doğru ağırlıklandırırsınız (bölüm 8.4).
Bazı terimlerin yürütülmesi gerekmez:
- Işın boyunca görülen ince yatay bir disk tam bir elipstir. Raptor'un Mach diskleri analitik kalır:
u² + ((s0 − sc)/tanE)² < rm². - Bir ışın boyunca integrallenen ince küresel bir kabuk kenarda parlaklaşır: parlaklık
rs·w / sqrt(rs² − d² + 2·rs·w)ile orantılıdır. Normalleştirilmiş profilin karesini alın; yoksa içi süt beyazı bir kabarcık gibi okunur.
5. Shader içeriğini SVG'ye hizalamak
Bir düzlem üzerinde yatan her şey o düzlemin SVG çizgileriyle tam olarak hizalanmalıdır.
- Aynı sayılar. SVG'nin planlarını ve yüksekliklerini shader'a
DATAüzerinden verin (dalga leğeniinside,outside,paper,base,round,zve gölge düşüren her şeyin planlarını verir) ve onlarıglLayer'a bir kezfixeduniform'lar olarak teslim edin (onunFIXED'i). İşaretli uzaklıklı birsdRectile aynı yuvarlatılmış dikdörtgenlere karşı test edin; böylece aydınlatılmış bir kâğıt köşesi çizilen köşe yarıçapında biter. - Aynı düzlem. Dalga leğeni camın arkasındaki su bandı için
onFloor(vb, uZ.x)(kâğıt),onFloor(vb, uZ.z)(su) veonWallX(vb, uNear.x)veonWallY(vb, uNear.y)(iki yakın cam duvar) kullanır. Asla ekran ötelemeleri kullanmayın. - Bir açıklıktan görülen, iki kez maskelenir. Daha yakın bir düzlemdeki bir açıklığın arkasındaki içerik (bir gözetleme camı, bir pencere, bir girinti, bezelinin altındaki bir gösterge yüzü) kendi düzlemiyle (
onWallY(vb, back)) ve açıklığın düzleminde test edilen açıklıkla (onWallY(vb, face)) maskelenir; iki yumuşak maske çarpılır. Yalnızca arka düzlemde maskelenen kör testli bir gözetleme camı, yağını ve camını üst kenarın yaklaşık 0.4 px ötesine gövde yüzüne taşırdı ve altta öteleme açıklığın içinde çıplak bir gövde şeridi gösterdi. Girintinin iç duvarlarını ön SVG'de, yönlerine göre gölgelendirerek çizin (craft.md, bölüm 10); böylece iki düzlem arasındaki öteleme çıplak yüz değil, duvar olarak okunur. - Yalnızca sapma hareket eder. Bir efekt gördüğünüzü kaydırıyorsa (dalgalardan kırılma), yalnızca dinlenme durumundan farklı olan kısmı uygulayın. Dalga leğeni dalgalı yüzey ve düz su tarafından bükülen görüş çizgisini hesaplar ve yalnızca farkı uygular. Dinlenmede suyun altındaki kâğıt SVG kâğıdıyla hizalanır. Kayma, kâğıda giden görüş çizgisinin bir ayağın birkaç birim yakınından geçtiği her yerde de sıfıra söndürülür (
legClear); böylece kırılan kâğıt orta SVG'nin üzerine çizdiği bir ayağın arkasından asla kayıp çıkmaz. - Yükseklik alanları torna profiliyle eşleşir. Raptor'un
ground(r)'si platform diski artı alev konisi profilidir;build.mjs'tekiG.solidOfprofiliyle aynı yarıçap ve yüksekliklerle. Koninin arkasındaki akış geometrik olarak gizlenir ve sert kenarlar yalnızca SVG'nin gerçek bir siluet çizdiği yerde görünür.
Hizalamayı 4×'te kontrol edin (bölüm 9).
6. API ve minimal bir sayfa
import { GLSL_ISO, glCamera, glLayer, smooth, burst, settle } from "../../kit/gl.mjs";
import { kitScript, glScript } from "../../scripts/inline-kit.mjs";
| Fonksiyon | Ne yapar |
|---|---|
glCamera(P) |
Bir kit izdüşümü için { origin, k, cam }. Node'da ve tarayıcıda çalışır. Sayfanızın DATA'sına koyun. |
glLayer({ canvas, fragment, view, camera, uniforms = [], fixed = {}, extensions = [], maxRatio = 1.5, onError }) |
GLSL_ISO + fragment'ı canvas üzerinde önceden çarpılmış alfalı bir WebGL1 bağlamıyla derler. view viewBox'ın [W, H]'si, camera glCamera(P)'dir ve uniforms her karede gönderdiğiniz uniform adlarını listeler. fixed, oluşturmada bir kez ayarlanan uniform'lar için { name: value }'dur (geometri, tema sabitleri). extensions GLSL uzantılarını listeler (["GL_OES_standard_derivatives"]); bağlamın sahip olduğu her biri etkinleştirilir ve bir #extension yönergesi olarak başa eklenir. { gl, extensions, set(values), draw(values, time), clear() } döndürür; burada extensions gerçekten etkinleştirilenleri listeler; WebGL eksikse ya da shader derlenemezse null döndürür (varsayılanı console.error olan onError günlüğü alır). |
set(values) |
Çizmeden uniform'ları ayarlar. |
draw(values, time) |
Tuvali CSS kutusu × min(maxRatio, devicePixelRatio) boyutuna getirir, kamera uniform'larını ayarlar, uTime = time yapar, values içindeki her değeri ayarlar ve tam ekran tek bir üçgen çizer. |
clear() |
Yalnızca bir şey çizildiyse şeffafa temizler. |
smooth(a, b, x), burst(age, rise, fall), settle(state, target, dt, seconds, calm) |
Her shader uniform'unun binmesi gereken JS zarfları (bölüm 7 ve motion.md). settle hâlâ hareket ederken true döndürür. |
glScript() |
export'u çıkarılmış kit/gl.mjs kaynağı; bağımsız bir sayfaya kitScript()'ten sonra gömmek için. O sayfadaki canlı kod glLayer, smooth, burst ve settle'ı doğrudan kullanır ve kendininkini bildirmemelidir. |
Bir uniform değeri (fixed, set ya da draw içinde) bir sayı (float), 2–4 uzunluğunda bir dizi (vec2–vec4) ya da uniform1fv ile bir uniform float name[N]'e gönderilen 4'ten uzun bir dizi veya Float32Array olabilir: 24 örnekli bir geçmiş tek bir uH[24]'tür. Bu çağrılar üzerinden int, matris ya da örnekleyici yoktur. Bir uniform dizisinin tutabileceğinden fazla veri için bir doku kullanın: bir sampler2D varsayılan olarak 0 birimini okur; bu yüzden onu layer.gl üzerinde oluşturun ve güncelleyin. Dalga leğeni 32 × 2'lik bir geçmişi değer başına 16 bitle bir RGBA8 dokuya paketler (PACK, put, unpack); her katmanın kendi bağlamı olduğu için katman başına bir doku. Bilinmeyen adlar ve derleyicinin optimize edip attığı uniform'lar atlanır.
Parçanız kendi ek uniform'larını ve önceden çarpılmış gl_FragColor yazan bir main() bildirir. Tuval, SVG'lerle tam olarak aynı kutuyu, aynı en boy oranıyla kaplamalıdır; çünkü viewOf tek bir tekdüze ölçek varsayar.
Minimal bir sayfa: kare bir platformun üstünde parlayan sıcak bir plaka. Plaka 40 yarıçaplı bir SVG halkasıyla (k.ring(0, 0, 40, 0, P, 72)) hizalanır ve parlama platformun yuvarlatılmış kenarında durur, çünkü ötesinde aydınlatılacak hiçbir şey yoktur.
build.mjs:
const P = k.fitProjection(points, W, H, { pad: 28, azimuth: 40 });
const DECK = { x: -90, y: -90, w: 180, d: 180, r: 8 };
const DATA = { W, H, camera: glCamera(P), plateR: 40, deck: [DECK.w / 2, DECK.r] };
const backSvg = k.figureSvg({ width: W, height: H, label: LABEL, body: back });
const frontSvg = `<svg xmlns="http://www.w3.org/2000/svg" class="iso-svg fx-layer" viewBox="0 0 ${W} ${H}" aria-hidden="true">${front.join("")}</svg>`;
const stage = `<div class="fx-stage" tabindex="0">${backSvg}<canvas class="fx-layer fx-gl" aria-hidden="true"></canvas>${frontSvg}</div>`;
const page = k.pageHtml({ title, theme, body: `<style>${CSS}</style>${k.plateHtml({ ...card, body: stage })}`, script: kitScript() + glScript() + LIVE.replace("__DATA__", JSON.stringify(DATA)) });
live.js:
const D = __DATA__;
const stage = document.querySelector(".fx-stage");
const still = matchMedia("(prefers-reduced-motion: reduce)");
const LIGHT = document.body.dataset.theme === "light" ? 1 : 0;
const FRAGMENT = `
uniform float uHeat;
uniform float uLight;
const float R=${D.plateR.toFixed(1)};
const vec2 DECK=vec2(${D.deck[0].toFixed(1)},${D.deck[1].toFixed(1)});
float deckDist(vec2 p){vec2 q=abs(p)-DECK.x+DECK.y;return length(max(q,0.))+min(max(q.x,q.y),0.)-DECK.y;}
void main(){
vec2 vb=viewOf(gl_FragCoord.xy);
float px=pixelOf()/uCam.z;
vec3 p=onFloor(vb,0.);
float r=length(p.xy);
float disc=1.-smoothstep(R-.7*px,R+.7*px,r);
float onDeck=1.-smoothstep(-.7*px,.7*px,deckDist(p.xy));
float halo=exp(-pow(r/(R*1.6),2.))*onDeck;
vec3 col=tonemap(vec3(1.,.42,.12)*(disc*.9+halo*.35)*uHeat,1.2);
float a=max(col.r,max(col.g,col.b));
vec4 dark=vec4(col,a);
vec4 light=vec4(vec3(.62,.24,.08)*a*.8,a*.8);
gl_FragColor=mix(dark,light,uLight);
}`;
const layer = glLayer({ canvas: stage.querySelector(".fx-gl"), fragment: FRAGMENT, view: [D.W, D.H], camera: D.camera, uniforms: ["uHeat"], fixed: { uLight: LIGHT } });
if (layer) stage.setAttribute("data-gl", "");
const heat = { x: 0, v: 0 };
let target = 1, clock = 0, last = 0, frame = 0, visible = false;
function tick(now) {
const dt = last ? Math.min((now - last) / 1000, 1 / 30) : 1 / 60;
last = now;
clock += dt;
const moving = settle(heat, target, dt, 0.35, still.matches);
if (layer) heat.x > 1e-3 ? layer.draw({ uHeat: heat.x }, still.matches ? 0.8 : clock) : layer.clear();
frame = visible && moving ? requestAnimationFrame(tick) : 0;
}
const run = () => { if (!frame && visible) { last = 0; frame = requestAnimationFrame(tick); } };
new IntersectionObserver((entries) => { visible = entries[entries.length - 1].isIntersecting; if (visible) run(); }, { rootMargin: "120px 0px" }).observe(stage);
uTime animasyonu olmadan bu döngü ısı oturunca durur. Canlandırılmış bir alanla (titreşim, akış) efekt etkinlik eşiğinin üstünde ve görünür olduğu sürece çalışmaya devam edin, aksi halde durun.
7. Her biri onu öğreten hatayla zanaat kuralları
Hiçbir şey patlamaz
Her uniform zamanda yükselişi ve sönümü olan pürüzsüz bir zarf boyunca hareket eder. Bir değeri asla tek karede 1 yapmayın.
- Tek seferlik olaylar
burstkullanır. İlk ateşleme parlaması tek karede 0'dan tama çıktı ve kartı beyaza boğdu. Şimdi her olay (parlama, şok, zemin halkası, kapanış patlaması, kart sarsıntısı)kit/gl.mjs'teki kitinburst(age, rise, fall)'unu, yani bir smoothstep yükselişi ve ardından üstel bir sönümü kullanır:
50–90 ms'lik bir yükselişle (başlangıçta sıfır eğim). Birconst flash = calm ? 0 : burst(sinceMain, 0.08, 0.13);t·e^(−t)alfa fonksiyonu sönümü yükselişe bağlar ve fazla uzun beyaz kalır. Tüm kart parlamalarını da düşük tutun: 0.9 opaklıkta bir CSS örtüsüyle birlikte bir parlama altı kare boyunca tüm kartı beyaza boğdu; bu yüzden örtü artıkflash × 0.32'dir. - Evre değişiklikleri fiziksel nicelikle sürülen çapraz geçişlerdir. Ana alev büyürken sönen ayrı bir ön yakıcı terimi anında bir renk değişimi gibi okundu. Şimdi renk, parlaklık, pürüzlülük, dil uzunluğu ve sıçrama hepsi yanma odası basıncının fonksiyonlarıdır ve basınç her evrenin hedefine doğru bir yayla hareket eder.
- Renk kendi yavaş zarfını alır. Basınçla sürülse bile turuncudan menekşeye değişim hızlı bir yayda smoothstep penceresini yaklaşık 0.23 s'de geçti ve parlaklık %33 düşüp 130 ms'lik parlamanın içinde geri geldi: bir parlamanın gizlediği bir kesim. Çözüm:
- Renk tonunun kendi kritik sönümlü zarfı vardır, kitin
settle'ı:settle(burn.hue, lit && burn.pc > 0.2 ? 1 : 0, dt, lit ? 0.32 : 0.5, calm),uHueolarak gönderilir. - Parlaklık, renk tonu yükselirken 1.15'ten 1.0'a monoton olarak düşsün diye JS'te hesaplanan ayrı bir
uGlow'dur. - Basınç yayı kritik sönümlüdür (sertlik 20, sönüm 2√20); böylece 318 bara aşmaz.
- Renk tonunun kendi kritik sönümlü zarfı vardır, kitin
- Bir sönme sırasında parlaklık yalnızca düşer. Kapanışta alev turuncu bir alev olarak yeniden parladı, çünkü
b < ailemix(a, b, hue)renk tonu söndükçe yeniden yükseldi. Şimdi parlama düzeyi kapanış başladığında donar ve ardından yalnızca azalan niceliklerle ölçeklenir. - Bir yayı değiştirdiğinizde komşuları yeniden kontrol edin. Basınç yayı kritik sönümlü yapılınca ön yakıcı dili neredeyse kayboldu. O ayağın kendi daha sert kritik yayına ihtiyacı vardı (sertlik 64).
- Tek seferlik bayraklar olaya aittir. Bir kapanıştan 2 s içinde yeniden ateşlemek bir sonraki kapanış patlamasını kaybetti, çünkü patlamanın saati yalnızca sessiz bir dönemden sonra sıfırlanıyordu. Şimdi buna yanma başına bir
poppedbayrağı karar verir. Saatiignite()içinde sıfırlamak hâlâ sönmekte olan bir patlamayı keserdi. - Değişiklikler ortam içinden yayılır. Dalga leğeninin frekansı değiştiğinde yeni dalga boyu tüm alanı bir anda yeniden döşemez. Her karede sayfa kaynağın gerçek fazını, ω'yı, k'yi, mm başına sönümü ve 1/grup hızını kaydeder, 50 ms arayla 32 örnek tutar ve bunları iki shader'a da 32 × 2'lik bir doku olarak yükler. Her nokta kendi geçmişinin grup hızıyla tek bir sabit nokta adımıyla
r / groupSpeedyaşını bulur ve k'yi ve sönümü o yaştan okur; böylece yeni dalga boyu gerçekte olacağı gibi daldırıcılardan dışa yayılır. - Bir değişiklik boyunca fazı sürekli tutun. İlk sürüm
θ = k(age)·r − φ(t)kullandı. Bir tarama sırasındar·dk/dtterimi uzak tepeleri yarıştırdı ve örtüştürdü (alias) ve frekans hareket ederken genliği sönümlemek (calm) deseni boşalttı: ortalama kâğıt parlaklığı %17 düştü. Şimdiθ = kE·r − uPhase + Ψ(age); buradaΨkaynağın o örnekten bu yana ω'sının öngördüğünün ötesinde kazandığı fazdır (packHistory'dekilag). Sabit bir noktada tepeler artık yalnızca ω değiştiği kadar hızlı hareket eder; bu yüzden genlik sönümüne gerek kalmaz ve küçük bir hareket bulanıklığı terimi (gMove, tepelerin kare başına ne kadar hareket ettiği) hızlı değişiklikler sırasında kostikleri yumuşatır. - İlk ışık söner olarak gelir ve açık temada yalnızca aydınlatır. Dalga lambası ilk görünümde 0.9 s içinde söner olarak gelir (
smooth(0, 1, (lampClock − 0.15) / 0.9)). Koyu temada SVG düz su dolgusu bir CSS değişkeni (--rt-lamp) üzerinden aynı değerle söner. Açık temada ilk sürüm çıplak kâğıttan başladı; bu yüzden çukurların mürekkebi lamba yanarken leğeni koyulaştırdı ve düz su dolgusu onu daha da koyulaştırdı. Şimdi su dolgusu WebGL çalıştığında gizlidir ve kâğıt ilk kareden (lamba 0) aydınlatılmamış grisinde (off) çizilir veuLightile aydınlatılmış mürekkebe doğru karıştırılır; böylece ilk ışık yalnızca aydınlatır. Kâğıt üzerindeki bir yayıcı için aynı kural tersinden okunur: mürekkebi yalnızca ışık geldikçe büyür ve sahne asla önce soluklaşmaz ya da koyuya düşmez (bölüm 8.2).
Sert uzamsal kesimler yok
Her terim herhangi bir sınırdan önce sıfıra ulaşır: bir if, bir sınırlayıcı şekil, bir düzlem ya da tuval kenarı. if'i yalnızca terimin kanıtlanabilir biçimde yaklaşık sıfır olduğu yerde bir optimizasyon olarak kullanın.
- Bir boncuk terimi aralığının iki ucunda bir
ifile keskin biçimde kesiliyordu. Artık smoothstep ile söner olarak gelir ve gider. - Alevin bir
ifkutusu, birs < .01 continueve birstep(ax, R0)hale maskesi vardı. Üçü de dikiş olarak görünüyordu. - Ekran uzayındaki bir parlama sütunu lüleden tuvalin dibine kadar uzanıyordu. Tuvalin son satırlarında hâlâ 11–19/255 alfa vardı; bu yüzden platformun altında düz yatay bir çizgiyle bitiyordu. Artık zeminle sınırlanmış dünya uzayında bir haledir ve son 10 satır ve sütun tam olarak 0 okur.
- Geniş bir
exp()parlaması ve zemin koordinatlı buhar tuval kenarlarında hâlâ görünüyordu. İkisi de artık tuvalin içinde sıfıra düşer. - Hücre gürültüsü yalnızca kendi hücresinin içine bakar; bu yüzden ötelemesi artı yarıçapı artı yumuşak kuyruğu hücrenin yarı genişliğini aşan bir özellik hücre kenarında düz kesilir.
offset + wobble + radius + tail'i yarım hücrenin altında tutun ya da 3 × 3 komşu hücreyi test edin. Kör testli bir kabarcık alanı bir merkezin hücrenin 0.31'i kadar merkezden kaymasına, 0.85'lik yarım hücreye karşı 0.86'lık bir erimle izin verdi. atankullanan radyal gürültünün bir açı boyunca bir dikişi vardı. Bunun yerine yön vektörününnoise3(vec3(dir, r·k − t))'sini kullanın.- Yüzde duraklı bir CSS
radial-gradientöğesinin dışında bitti. Gradyan onun içinde bitsin diyecircle closest-sidekullanın.
Dünya uzayı, kamerada
- Ateşleme şoku izdüşürülmüş çıkışın çevresinde 2B bir daireydi. Alt yayı platformun üstünden ve altından geçti ve iki kolonun da üzerine boyadı. Artık gerçek bir küredir: her piksel ışını onu iki kökte vurur, zeminin altındaki kökler yumuşakça gizlenir, her kök kenar ağırlıklıdır ve her biri motor vekiline göre arka ya da ön tuvale gider.
- Zemin sıçraması su püskürtme halkasının üzerine düz yatırılmıştı; bu yüzden boru hacmini kaybetti. Halka artık
max(dot(n, toPlume), 0.)ile aydınlatılan bir torus örtücüdür. - Yakın kolonun arkasındaki pus ön yüzünün üzerine boyuyordu. Kolonlar artık shader'da opak kutulardır.
Katmanlama ve karıştırma
- Koyu temada yalnızca ışık yayan tuvaller
mix-blend-mode: screenkullanır ve alfa = en büyük kanal olacak şekilde önceden çarpılmış renk yazar. Örttüğünü de koyulaştıran bir tuval normal karışımda kalır: dalga leğeninin suyu içinden görülen kâğıdı renklendirir ve screen karışımı bunu sessizce düşürürdü. Açık temada her tuval normal karıştırılır ve mürekkebi koyu tema renginden koyulaştırılarak değil, kâğıdın göstermesi gereken renkten çözülür (bölüm 8). - Beyaz-sıcak çekirdeğin üzerine normal karışımla çizilen bulanık turuncu bir kenar lüle dudağının altında donuk kahverengi bir yaka gibi okundu ve dudaktan biraz kaymış çizildiğinde çift bir yay yaptı. Bir shader üzerindeki parlama dış hatları artık kendi kaplama SVG'lerine (
.rp-glow) gider; koyu temada screen ile karıştırılır, tam dudakta. Açık temada hiçbir şey ışık ekleyemez; bu yüzden bir kenar mürekkebin kendisinin bir parçasıdır: soluk bir çekirdeğin çevresinde gövdenin kenarında doygun bir bant (bölüm 8.1). - Çanın önünden geçmesi gereken buhar hep onun arkasındaydı. Artık motor vekiliyle iki tuvale bölünmüştür (bölüm 2).
Tonemap ve titreşim
- Asla
clampdeğil,tonemap()kullanın; böylece parlak çekirdekler düzleşmek yerine yumuşakça doyar. - Çıktı titreşimi olmadan büyük yumuşak terimler 8 bitlik merdivenlere dönüştü (düzey başına 18–34 px'lik uzunluklar) ve screen karışımı onları kötüleştirdi. Tonemap'ten sonra yaklaşık ±1 LSB'lik canlandırılmış üçgen gürültüyle titreştirin; boş pikseller tam 0 kalsın diye kapılı olarak (Raptor'un
inkOf'u):col=tonemap(col,1.15); float m=max(col.r,max(col.g,col.b)); float n=(hash12(gl_FragCoord.xy+fract(uTime*.61)*91.)+hash12(gl_FragCoord.yx*1.37+fract(uTime*.37)*57.)-1.)/255.; col=max(col+n*smoothstep(0.,1.5/255.,m),0.); - İnce ayrıntıyı bant sınırlayın. Dalga leğeni izdüşürülmüş dalga boyu 2.5–5 aygıt pikselinin altına düşen her dalgayı söndürür (
fineterimi). Aksi halde 28 Hz'lik bir desen moireye dönüşür.
Yalnızca önemli olduğu sürece çizin
- Cihaz oranını 1.5 ile sınırlayın (
maxRatiovarsayılanı). Yalnızca efekt etkinken çizin: Raptor her zarf 1e-3'ün altına inince iki tuvali de temizler ve döngünün durmasına izin verir. Şeklin geri kalanıyla aynıIntersectionObserverile ekran dışında durun veresize'da yeniden çizin. - Hiç dinlenmeyen bir efekt (dönen bir akıştaki kabarcıklar, bir pilot alev) shader'ı boştayken de çizdirir, ama şeklin geri kalanını değil: döngüyü bölün ki SVG işi (derinlik sıralaması, göstergeler, okuma değeri) yalnızca içinde bir şey değiştiğinde çalışsın ve efekti tek başına, yavaşsa düşürülmüş bir hızda çalıştırın. Kör testli bir tezgâh, bir zarfı 0 yerine 0.54'te oturduğu için tüm tick'ini sonsuza dek 60 fps'de tuttu.
- Maliyetli terimleri uniform kontrolleriyle kapılayın (
if(uSmoke<.0004&&uVent<.0004)return vec3(0.);). Bunlar tüm çizim için aynıdır; bu yüzden kapalıyken hiçbir şeye mal olmazlar.
Azaltılmış hareket
Doğrudan temsil edici kararlı bir duruma atlayın (Raptor ana aşamaya, dalga leğeni mevcut f ve d'sine atlar), uTime'ı dondurun (Raptor 0.8 kullanır), her olay zarfını sıfırlayın (parlama, şok ya da sarsıntı yok) ve yalnızca uniform'ların JSON'u değiştiğinde yeniden çizin. Şekil yine de girdiye tepki vermelidir.
Yedek
glLayer null döndürebilir; bu yüzden her çağrıyı koruyun. WebGL çalıştığında sahneyi işaretleyin (data-gl); böylece CSS SVG yedeği ile shader arasında geçiş yapabilir. WebGL'yi saplayarak test edin (bölüm 9).
8. Açık tema
Yayınlanan tema açık temadır. Açık bir sayfa, sonunda dönüştürülmüş koyu kart parlamaları değil, kâğıt için tasarlanmış shader'lar alır. İzometrik sitedeki yedi şekil koyu bir kart üzerine kuruldu ve sonra dönüştürüldü: bir havuz reaktörü, bir döküm, bir art yakıcı, bir cam fırını ağzı, bir tokamak, bir plazma kesici ve bir demirci ocağı. Her dönüşüm üç inceleme turu sürdü ve bu bölümdeki her kural onların bulgularından biridir.
Raptor'un açık tema alevi (examples/raptor-engine/live.js'teki inkOf) ne yapılmaması gerektiğini gösterir. Her renk tonunu sabit koyu bir kırmızıyla, vec3(.62,.34,.3), çarpar ve alfayı parlaklığa ayarlar; bu yüzden en sıcak gaz en koyu ve en opak çıkar. Gözden geçirenler buna "kırmızı mürekkep dumanı" dedi. Onu kopyalamayın. Dalga leğeninin gri mürekkebi (uPale) parlama olmayan ışık için hâlâ iyi bir modeldir: bir ton eğrisi 1 − uExpo.x/(1 + (e/uExpo.y)^uExpo.z) tepeleri neredeyse kâğıt beyazına ve çukurları kitin grilerine taşır ve suyu yalnızca düz sudan olan değişikliği yazar.
8.1 Kâğıt üzerinde mürekkep
Beyazın üzerinde tuval normal karıştırılır, önceden çarpılmıştır ve asla screen değildir, çünkü beyaz üzerinde screen beyazdır. Hiçbir piksel kâğıttan parlak olamaz; bu yüzden ışık doygunluk ve yerel kontrast üzerinden okunmalıdır:
- Bir parlama doygun bir gövde, soluk, beyaza yakın bir çekirdek ve renkli bir kenardır. Soluk çekirdek yalnızca çevresindeki gövde doygun ve onun çevresindeki zemin daha koyu olduğu için sıcak okunur.
- Işığın düştüğü şey bir renk tonu alır (multiply benzeri, sınırlandırılmış alfayla). Daha parlak olmaz.
- Işığı tutan hacim fiziksel bir nedenle daha koyudur: kurum, refrakter, derin su ya da gölge (bölüm 8.3). Asla siyah bir kutu değildir.
Kâğıdın göstermesi gereken rengi tasarlayın, sonra mürekkebi çözün. Işık enerjisinden okuyucunun beyaz üzerinde görmesi gereken renge giden bir rampa yazın. Sonra beyaz üzerindeki normal birleştirmeyi tersine çevirin; bu rgb + (1 − a) gösterir:
vec4 inkFor(vec3 seen,float cover){
vec3 absorbed=1.-seen;
float a=max(max(absorbed.r,max(absorbed.g,absorbed.b)),cover);
return vec4(vec3(a)-absorbed,a);
}
Beyaz üzerinde bu tam olarak seen'e birleşir.
- Reaktör.
inkOfbunupaperOf(e)rampasıyla yapar; buradae = 1 − exp(−2.3·max(lin))'dir. Rampa beyaz →TINT (.85,.93,1)→BODY (.2,.46,.94)(e .12–.5) →RIM (.2,.68,1)(.4–.65) →ICE (.9,.98,1)(.6–.9) şeklinde ilerler. - Art yakıcı. Onun
paperOf'u beyaz → gövde → soluk bir çekirdek karıştırır ve sonra aynı şekilde tersine çevirir. coversıcak uçta bir alfa tabanıdır: reaktörde WASH .52 ileWASH·smoothstep(.6,.9,e)ve art yakıcıda COVER .94 ileCOVER·smoothstep(.45,.92,m). Onsuz beyaza yakın bir çekirdeğin alfası 0'a yakındır; bu yüzden arkasındaki SVG çizgileri doğrudan görünür ve ışık olarak değil bir delik olarak okunur.
Diğer şekiller vec4(colour·a, a) biçimini kullanır: colour bir rampadan, a enerjinin doyan bir eğrisinden gelir. Tokamak'ın glowInk'i bir örnektir. İki biçim de çalışır; inkFor ayrıca okuyucunun gördüğü rengi garanti eder.
Işık türü başına bir mürekkep, shader'da birleştirilir. Ayrı biriktiriciler tutun, her birine kendi mürekkebini verin ve onları vec4 over(vec4 top,vec4 under){return top+under*(1.-top.a);} ile arkadan öne yığın:
| Terim | Mürekkep | Görüldüğü yer |
|---|---|---|
| Yayılım, sıcak şeyin kendisi | rampa gövdesi artı soluk bir çekirdek, .96–.97'ye kadar alfa | plazma kesici bodyInk, ocak barInk, tokamak glowInk |
| Bir yüzeye düşen ışık | sıcağa ya da soğuğa çekilmiş ton, .5–.8'de sınırlandırılmış alfa | döküm castInk ve tintInk, tokamak tintInk (.5), plazma kesici tintInk (.75–.8) |
| Pus: buhar, duman, duman gazı, delinme gazı | gri ya da hafif renkli, alfa 1 − exp(−density·k) |
plazma kesici hazeInk, tokamak gasInk, ocak buharı |
| Kıvılcımlar ve izler | her şeyin üstünde | döküm ve ocak sparks |
Plazma kesicinin lightInkOf'u en açık örnektir: over(arcCore, over(bodyInk(body), over(arc, over(fumeInk, over(steamInk, tintInk(tint)))))). Enerjileri önce toplayıp toplamı mürekkeplerseniz her şey tek bir renk tonu alır ve soluk çekirdek kaybolur.
Bitiriş.
- Her toplamdan sonra
rgb ≤ a'yı sınırlayın: art yakıcı 1 LSB'lik bir önceden çarpma taşması gösterdi. - Son önceden çarpılmış değeri, rgb'yi ve alfayı birlikte,
smoothstep(0.,1.5/255.,a)ile kapılı olarak titreştirin (cam fırınınınpaperOut'u, dökümündithered'ı). - Tuval kenarlarını tam 0'da tutun.
- Enerjiyi bir rampaya ulaşmadan önce tonemap'leyin (
tonemapya da1 − exp(−k·e)), aslaclampdeğil.
8.2 Mürekkep ışıkla büyür
Koyu temada daha fazla ışık yalnızca ekler. Kâğıt üzerinde daha fazla ışık daha fazla mürekkep demektir ve dikkatsiz bir formül ışık azalırken mürekkep ekleyebilir ya da ışık artarken mürekkep alabilir. Gözden geçirenlerin en sık bulduğu hatalar bunlardı:
Bitmiş mürekkebi bir kez söndürün. Önceden çarpılmış bir mürekkebi
uile ölçeklemek, çıplak kâğıttan tam resme tam doğrusal bir çapraz geçiştir, her pikselde monotondur: reaktörünink*=uFirst'ü, cam fırınınınpaperOut(overInk(gas, ink)*uLit)'i. Cam fırını ağzı önce her terimi mürekkeplemeden önce söndürdü (holeOf'ta, port deliğinde ve topta*uLit). Soluk çekirdek doygun gövdesinden önce geldi, sonra zemin koyulaştı; bu yüzden ilk ışık önce soluklaştı sonra koyulaştı. Her açık tema mürekkebini söndürülmemiş tonlardan kurun (onunlitOf()'u açık temada 1 döndürür) ve yalnızca sonucu söndürün.Bir parlamayı enerji ve alfayla birlikte söndürün. Yalnızca alfayı söndürürseniz soluk bir çekirdek soluk kalır ve incelen parlama süt grisine döner: tokamak'ın temas bandı böyle yaptı. Yalnızca enerjiyi söndürürseniz parlama rampasından aşağı geri koşar. Tokamak artık enerjiyi bir tabana kadar ve alfayı yolun geri kalanında ölçekler:
hot+=glowInk(c*level*(.3+.7*hold))*hold*(1.-hot.a). Bant giderken pembe-leylak kalır.Renk tonunu sabit parlaklıkta alın. Enerji düştükçe tonemap renk tonunu kızartır ve koyulaştırır ve alfa sınırına yakınken mürekkep artar: art yakıcının boşluğu alev sönerken %55 ağırlaştı. Çözüm:
- Renk tonunu almadan önce yanmayı yerel aydınlatma gücüne bölün (
burn/lit,lit = la.x·glowOf(la.y)/LIT_GLOW). - Renk tonlarını her parçanın enerjideki payına göre karıştırın.
- Alfayı enerjiye bağlayın:
cap·(1 − exp(−1.6·e/cap)). - Düşük örtüde renk tonunu
EMBER'e doğru ısıtın ve alfayıinkOf(hue)/inkOf(ember)ile ölçekleyin; böylece mürekkep aynı kalır.
Bununla pencerenin mürekkebi kesimde 1894'ten 155'e monoton olarak düştü.
- Renk tonunu almadan önce yanmayı yerel aydınlatma gücüne bölün (
Devirler düz önceden çarpılmış karışımlardır. Art yakıcının jetinin iki kopyası vardır, kuru mavi ve aydınlatılmış sıcak,
mix(blue, warm, lift)ile çapraz geçirilir. Orada birmax(alpha)örtüsü kuru jetin mürekkebini ikiye katladı. Ayrıca her kopyayı diğerinin terimlerini alamayacak şekilde kapılayın: kuru şok diski sıcak kopyaya krem rengi bir yama olarak sızdı.Çarpan zarflar bir sönme sırasında yükselebilir. Bir ilk ışık zarfı hâlâ yükselirken efektin kendi kenarı düşüyorsa sönme aydınlanır: tokamak ilk yarım saniyesinde Home'a basıldığında bunu yaptı. Efekt düşerken ilk ışık büyümesini dondurun.
SVG yedeğinden devir tamdır; yoksa shader gelirken sahne çukura düşer (bölüm 8.5).
8.3 Işık için bir zemin
Çevresinde hiçbir şey olmayan beyaz üzerindeki bir parlama bir leke gibi okunur. Ona gerçekten sahip olduğu zemini verin:
SVG'deki boşluklar daha koyu bir belirteç kümesi alır. Bir grup sınıfı içindeki her şey için kit belirteçlerini yeniden tanımlar:
- art yakıcının
.ab-inside'ı: kâğıt#4a453f, gölgeler#3c3833'ten#544e47'ye, çizgiler#5e5952'den#9b958b'ye; - tokamak'ın
.tk-cavity'si:#66646e'den#8b8992'ye; - dökümün isli delik tonları (
--fd-bore-*-sooted,--anatomy-shade-0ile--fd-soot-ink: #2f2a25'in bircolor-mix'i); - cam fırını ağzının
--gf-voidve--gf-bore'u; - reaktörün su sütunu gradyanı (
--rx-water-top,-mid,-deep,#edf2f4'ten#c9d7e0'a).
Koyu temada aynı belirteçler
inherityapılır. Dört gölge tonunun hepsi kalır; böylece iç kısım hâlâ biçime sahiptir ve bir delik yerine refrakter, kurum ya da derin su olarak okunur.- art yakıcının
Zemin ışığın renk tonunu canlı olarak alır. Aynı belirteçler döngünün yazdığı bir değişkenle ışığın renk tonuna doğru
color-mixyapılır: yüzlerde %30 ve çizgilerde ve arka plakalarda %55 ile--ab-flame, delinmenin menekşesi için--tk-cold-mix. SVG zemini böylece WebGL olmadan da alevle ısınır.Shader'ın zemini SVG'ninkine eşittir. Shader'ın opak bir boşluk boyadığı yerde (cam fırını ağzının
holeOf = overInk(emberInk(t,1.), vec4(SOOT,1.))'i) belirteci yeniden yazmak yerine okuyun. Döküm bir sonda yolu ekler, dolgusunu hervar(--fd-…)'e ayarlar,getComputedStyle(probe).fill'i okur ve sonucu birfixeduniform olarak verir (onunmount.ts'indekipaintOf).Kaynağın çevresindeki ışık uzun kuyruklu bir renk tonudur. Tokamak'ın kabı, uzun menzilli bir yıkama (
skin*(.14*exp(-gap/(.22*U))+.17*exp(-gap/(.9*U)))) duvarlarını macentaya boyayana kadar düz tepede gri okundu. Kolonda ölçüldü: plazma kapalıyken#6e6c76, yıkama .17'deyken#786778; bu aydınlatılmış okunur. .1'de hâlâ gri okunuyordu.Boş havada bloom yok. Açık temada havada asılı bir akıntının ya da levhanın çevresindeki bir hale kâğıt üzerinde bir lekedir (dökümün akıntı ve çentik parlamaları öyleydi). Açık temada havadaki haleleri kaldırın. Kenar halkasını gövdenin içinde ve renk tonunu ışığın ulaştığı yüzeylerde tutun.
Işık yalnızca kaynağın görebildiği yere düşer. Reaktörün yüzey ışığı, sızdırmazlık testi platform yerine tank dudağının gerçek üstünü (
RIMZ) kullanana kadar bir kalkanın kesik yüzüne ve başlığına sızdı. Plazma kesici, jet görünür biçimde bir sonraki bölmedeyken jetin alt ucunun altındaki lata bölmesini aydınlattı; onunbayLight'ı artık her bölmeyi jet sütunundaki payına göre aydınlatır; böylece jet hareket ettikçe ışık bölmeden bölmeye çapraz geçer.
8.4 Olguya göre tarifler
Sıcak gövdeler mat kırmızı → turuncu → sarı → beyaza yakın sırasıyla ilerler, bu sırayla ve başka hiçbir sırayla değil. Modelde tek bir durak tablosu tutun, onu GLSL'ye derleyin ve yedek için aynı tabloyu kullanın: dökümün INK_STOPS'u (inkRampGlsl ve SVG için glowInk), ocağın STEEL_INK'i (steelInk, steelInkOf).
- Erimiş metal, dövülmüş çelik, kütükler, kesim izleri.
- Tabloyu parlaklık sıcaklığına göre indeksleyin; böylece donuk uzak bir duvar ve parlak bir döküm aynı tablodan okunur. Dökümün
brightOf'u Wien'i yeşil kanaldan tersine çevirir. - Ocağın durakları: 560 °C
(.45,.09,.05), 680(.56,.07,.04), 780(.78,.10,.03), 950(.95,.38,.06), 1150(1,.78,.12), 1260(1,.93,.78). - Alfa görünür parlamada açılır: 560–820 °C üzerinde
steelGlowsya da 470–590 °C üzerinde dökümünINK_FROM/INK_FULL'u, metalin kendi zemin tonundan çapraz geçerek. - Kenar daha serin ve daha koyudur (
steelInk(t − 140), dökümün 150 K'lik kenar düşüşü). Krem bir çekirdek yalnızca en sıcak merkezde belirir (1050–1280 °C üzerindesteelPale, .8'de). - Parlamanın altında koyu bir kabuk durur ve metal soğudukça büyür: ocağın
barInk'i oksitten(.28,.29,.32)kabuğa(.24,.22,.23).86'ya kadar örtüyle ilerler. - Gözden geçirenlerin döküm akıntısı için sözleri: "turuncu kabuklu krem bir çekirdek".
- Tabloyu parlaklık sıcaklığına göre indeksleyin; böylece donuk uzak bir duvar ve parlak bir döküm aynı tablodan okunur. Dökümün
- Cam toplamaları.
- Alfa bir parlaklık kapısıdır:
GLOW_SEEN.008–.028, yaklaşık 600–710 °C. - Renk
pow(hue, mix(1.6,.8,warm))*mix(.62,1.,warm)'dır; koyu vişne → kırmızı → turuncu ilerler. - Soluk çekirdek (
CORE_PALE (1,.88,.62)) sabit bir yarıçapta değil, pikselin kendi yayılımının en parlak olduğu yerde durur (cored, parlaklık .14–.24). - Serin kabuk (
SKIN_COOL (.3,.11,.07)) yalnızca geri çekilir. - İlk sürümün somon renkli bir aşaması vardı. Beyaza doğru karıştırılan bir kırmızı somondur ve hiçbir sıcaklık böyle görünmez: bir şey soluklaşmadan önce turuncu ve sarıdan geçin.
- Alfa bir parlaklık kapısıdır:
- Kıvılcımlar.
- Doygun bir baş ve daha koyu, daha serin bir kuyruk. Plazma kesicinin
tailCooling'i kuyruğu başın sıcaklığının 0.62'sine ayarlar; ocağın kuyruk mürekkebi(.42,.07,.02), .8'de sınırlandırılmış .75 alfadır. Yalnızca bir kıvılcımın en genç üçte biri soluk bir çekirdek alır (u < .35). - İzi en az yaklaşık 1.25 aygıt px genişliğinde tutun (
max(.35+.6px, 1.25px)); yoksa çapraz izler 10–24×'te boncuklanır. - Beyaz üzerinde bir kıvılcım mürekkeptir; bu yüzden daha sıcak bir yayıcının üzerinde kaybolmalıdır. Döküm kıvılcım mürekkebini hem metale hem parlayan deliğe karşı
1 − cover·smoothstep(sparkT−60, sparkT+60, tb)ile çarpar. - Bir jetin arkasından geçen bir kıvılcım derinliğe göre söner:
cover*smoothstep(-.4,.4,tJet-tS).
- Doygun bir baş ve daha koyu, daha serin bir kuyruk. Plazma kesicinin
- Alev ve egzoz.
- Yarı saydam kenarlı doygun turuncu-sarı bir gövde, açık temada eksene doğru ağırlıklı:
mix(1.,1.3-.6*smoothstep(0.,rf,r),uLight). - Kenar mantosunu kesin (
mix(.8,.3,uLight)). Koyu bir aleve kenarını veren kenar kabuğu (bölüm 4) kâğıt üzerinde bir dış hat çizer ve alevi içi boş bırakır. - Pürüzlülüğü dış kısımla sınırlayın, sıcak ekseni genişletin ve soluk çekirdeği lüleye koyun; uzunluğun .4–.9'u boyunca sönerek.
- Bir kesit kontrol edin. Art yakıcının son alevi kenardan kenara mürekkepte
0 2 13 33 31 49 78 72 78 52 40 32 8okur. - Kuru jet soluk mavi bir sütundur. Şok elmasları sıcak kenarlı ve yumuşak kenarlı (disk yarıçapının yaklaşık .65'i) soluk krem disklerdir (
CREAM (1,.95,.86)). Daha güçlü itildiğinde ikinci bir elmas bir topuz ya da bir pul gibi okundu; bu yüzden soluk bir mercek olarak kalır.
- Yarı saydam kenarlı doygun turuncu-sarı bir gövde, açık temada eksene doğru ağırlıklı:
- Duman ve buhar.
- Açık gri, asla üzerine koyu bir parlama mürekkebi yatırılmadan. Kenar
(.84,.87,.90), tepe(.97,.975,.98)ve bulutun kendi gölgesiyle alt taraf(.72,.76,.81); alfa1 − exp(−density·k). - Sıcak bir kaynağın yakınında bulutu soluk kehribara doğru ısıtın (
mix(steelInk(T), CREAM, .5)). Ocak önce bulutun üzerine parlama mürekkebi yatırdı ve gözden geçirenler somon renkli bir leke gördü. - Bir su verme bulutu küvetten ayrılan bir
burst'tür: suyun 4 biriminden yaklaşık 40 birim yukarıya giderek yavaşlayarak tırmanır (ocağınshader.ts'indekiPUFF), kenarı yaklaşık 0.15 s içinde aşar, 7'den 13'e genişler, büyüdükçe incelir ve sütunun yükseklik penceresini paylaşır; böylece asla kesilmez.
- Açık gri, asla üzerine koyu bir parlama mürekkebi yatırılmadan. Kenar
- Plazma (tokamak).
- Doygun macenta-menekşe:
inkBody(chroma)=pow(chroma,vec3(1.8))*vec3(.93,.86,.97). - m .55'in üstünde çekirdek, kromanın %8'i korunarak beyaza doğru soluklaşır.
- Alfa:
.97*smoothstep(.02,.42,m)*(1.-exp(-2.6*(m+2.*m*m))). - Delinme gazı menekşe bir pustur,
(.52,.44,1)'den(.74,.68,1)'e, alfa en fazla .64. Kesitten görüldüğünde kabın içi boşluk belirteç kümesini kullanır.
- Doygun macenta-menekşe:
- Cherenkov (reaktör).
- Mavi renkli bir su sütununda canlı camgöbeği-mavi: SVG su gradyanı üzerinde 8.1'deki kâğıt rampası, sıcak uç
WASHile tabanlanmış; böylece çekirdek arkasındaki yakıt hatlarını örter. - Kritik üstündeki darbeyi sıkıştırın (
1.+.75*log(level)); böylece 1.5 GW'lık bir darbe kartı taşırmadan havuzu aydınlatır. - Salon duvarındaki parlama ayrı, daha zayıf bir mürekkep kullanır (
HALL_INK.35).
- Mavi renkli bir su sütununda canlı camgöbeği-mavi: SVG su gradyanı üzerinde 8.1'deki kâğıt rampası, sıcak uç
- Plazma arkı (plazma kesici).
- Soluk bir çekirdekli
vec3(.96,.95,1.)menekşe bir arkvec3(.6,.34,1.). - Plaka ve su üzerinde sıcak renk tonu mürekkebi:
mix(vec3(1.,.5,.08),vec3(1.,.68,.3),…), alfa.75*(1.-exp(-3.2*sqrt(m))).
- Soluk bir çekirdekli
8.5 Açık temada SVG ve yedek
- Temayı bir kez okuyun. Temayı derleme zamanında seçin (
pageHtml({ theme })ya da React sarmalayıcısındadata-theme; izometrik sitede şeklin derlemesithemedöndürür veLivePlatekoyu bir kartı kendi.iso[data-theme="dark"]'ına sarar). Canlı kod onu en yakın[data-theme]'den okur (root.closest("[data-theme]")?.getAttribute("data-theme") !== "dark") veuLight'ıfixedolarak verir. Her yalnızca açık tema değişikliğiniuLight'a kapılayın (mix(dark, light, uLight)ya da uniform üzerinde bir dal); böylece koyu çıktı olduğu gibi kalır. Ucuzsa koyu temayı çalışır tutun. - Renk sabiti yok. Bir şekil CSS'inde koyu renkleri sabit kodluyorsa onları koyu geçersiz kılmalı belirteçlere dönüştürün:
.x { --x: light }sonra.iso[data-theme="dark"] .x { --x: dark }. Bir belirteçle eşleşmesi gereken shader sabitleri bir sondadan gelir (8.3). - İç içe temalar.
.iso:not([data-theme="dark"]) .x, koyu bir kartın çevresindeki açık sayfa üzerinden de eşleşir. Reaktörün koyu yedeği bu yüzden siyaha çarpıldı. Koyu kuralı açık olandan sonra, eşit özgüllükte koyun ya da kartın kendi temasına bağlayın: ocak.iso-plate[data-plate="light"]kullanır. - Yedek nasıl karışır. Onun doygun renk tonlarını multiply yapın; bunlar koyulaştırır ve altlarındaki çizgileri görünür bırakır. Soluk çekirdeklerini normal karıştırın, çünkü multiply aydınlatamaz: reaktörün WebGL'siz çekirdeği .85'te
#fff'ten .3'te#dcefff'e normal karıştırılmış radyal bir gradyandır.- Yedek renkleri shader'la aynı rampadan gelir: dökümün
glowLightCss'i ve shader'ın toplama mürekkebini tam olarak yansıtan cam fırınınıngatherInk'i. - Soluk bir belirteç üzerindeki opaklık griye söner; bu yüzden yedeğe doygun renk tonunu verin: tokamak'ın yedek halkaları
--anatomy-lit: #e0409cve--anatomy-hi: #f27ec0ayarlar.
- Yedek renkleri shader'la aynı rampadan gelir: dökümün
- Yedek şekil değişiklikleri çapraz geçer. Tokamak'ın WebGL'siz plazma halkası önceden hesaplanmış 11 halkadır (
LIMITED_RINGS). Komşu halkalar, toplamı 1 olan doğrusal ağırlıklarla, shader'ın çizdiği aynı yumuşatılmış yarıçapta çapraz geçirilir. 6 halkayla sönmenin ortasında çift bir dış hat göründü. - Yarı saydam bir mürekkebin altına boşluklu hiçbir şey girmez. SVG'nin çizdiği her şey tamamen opak olmayan bir mürekkebin içinden görünür. Ocağın ısı bandı aralarında kâğıt olan bir kontur kümesiydi ve çubuğun %14'lük saydamlığından gri çizgiler bastı. Artık ısı gradyanıyla doldurulmuş bir yüzdür.
- Yedekten shader'a devir tamdır. İlk ışıkta tuval mürekkebi
uile yükselirken altındaki yedek gider. Yedekte doğrusal bir1 − usönmenin ortasını iki uçtan da soluk yapar. Tam doğrusal bir çapraz geçiş için yedeğin opaklığını(1 − u)/(1 − u·α)yapın; burada α tuval mürekkebinin önemli olduğu yerdeki alfasıdır: cam fırınının--gf-gather-fade'i, α toplamanın merkez durağı. Diğer yedekleri shader neredeyse gelene kadar tam güçte kalır:clamp(0, calc(var(--gf-fallback, 1) * 12), 1). - Shader'ın çizdiği her gövdenin bir yedeği vardır. Dökümün akıtma akıntısının hiç yoktu; bu yüzden WebGL olmadan eksikti. Artık shader'ın okuduğu aynı geçmişten her karede yeniden kurulan bir SVG dış hattıdır.
8.6 Mürekkebin bir çizgiyle buluştuğu yer
Tuval arka SVG'nin 0.6 px'lik konturlarının üzerinde yatar. Açık temada yanlış hizalanmış bir kenar koyu bir dikiş olarak değil, soluk bir boşluk ya da konturun üzerinde renkli bir çizgi olarak görünür.
- Konturun iç kenarında durun. Bir yüzün mürekkebini konturun altında tama çıkarın ve konturun iç kenarında, çizgi kalınlığının yarısı kadar içeride bitirin: kitin 0.6 px'lik çizgileri için 0.3 vb. Hesaplanan kontur genişliğini ölçün; ocağın gözden geçireni 1.1 tahmin etti. Örnekler:
SEAM=.3ile reaktörüncutFade=smoothstep(-SEAM-.5*px,-SEAM+.5*px,gap)'i ve ocağınstrokeMeet'i. Bir renk tonunu aynı yarım konturla doldurun (dökümünringLight'ı); böylece asla çizginin üzerine boyamaz ve içinde soluk bir bant bırakmaz. - Hiçbir şeyin konturlanmadığı yerde mürekkebi temizlemeyin. Ocağın örtüsü iki yüzün paylaştığı, SVG'nin konturlamadığı kıvrımda temizlendi ve çubuk boyunca beyaz dikişler göründü. Böyle bir kıvrımı bunun yerine mürekkeple çizin: ×0.72, en az 0.8 tuval px genişliğinde (daha keskini noktalı çıktı).
- Komşu ışınların uyuşmadığı yerde süper örnekleyin. Reaktör dört alt piksel ışını izler ve isabetleri, derinlikleri, türleri, normalleri ya da kesim payları farklı olduğunda (
apart) 16 örnek alır; böylece kesik yüz kenarları merdiven yapmaz. dpr 1'de bir tuval pikseli yaklaşık kontur kadar geniştir; bu yüzden bir piksellik yumuşaklık kalır. - Çerçevenin dışından giren bir gövde ekran uzayında tuval kenarından söner olarak gelebilir. Dökümün akıtma akıntısı, tuval üstünün 10.5 aygıt px altından başlayarak 11 vb boyunca söner olarak gelir; böylece kenar hâlâ 0 okur ve akıntı yukarıdan geliyormuş gibi okunur.
8.7 Gözden geçirenlerin gördüğü ve çözümü
| Açık temada gördüğünüz | Neden | Çözüm |
|---|---|---|
| Kırmızı-kahverengi bir "mürekkep dumanı" alevi | Renk tonu × koyu bir çarpan, alfa parlaklığa ayarlı | Bir kâğıt rampası: doygun gövde, soluk çekirdek, alfa tabanı (8.1) |
| Dış hat olarak çizilmiş, içi boş bir alev | Koyu kenar mantosu | Gövdeyi eksene doğru ağırlıklandırın, mantoyu kesin (8.4) |
| İlk ışık önce soluklaşıyor, sonra koyulaşıyor | Terimler mürekkeplenmeden önce tek tek söndürülmüş | Bitmiş önceden çarpılmış mürekkebi bir kez söndürün (8.2) |
| Bir parlama süt grisinden geçerek sönüyor | Soluk bir mürekkebin yalnızca alfa sönmesi | Enerjiyi ve alfayı birlikte kısın (8.2) |
| Ateş söndükçe bir boşluk koyulaşıyor | Alfa sabitken tonemap renk tonunu kızartıyor | Sabit parlaklıkta renk tonu, enerjiden alfa, köz eşleştirmesi (8.2) |
| Cam ya da çelik soğurken somon renkli bir aşama | Beyaza doğru karıştırılmış kırmızı | Kara cisim sıralı bir rampa, yalnızca sarıdan sonra soluk, çekirdek yayılımın zirve yaptığı yerde (8.4) |
| Kıvılcımlar metalin üzerinde kir gibi görünüyor | Daha sıcak bir yayıcının üzerinde koyu mürekkep | Kıvılcımları daha sıcak gövdelerin üzerinde boğun; doygun baş, daha koyu kuyruk (8.4) |
| Kıvılcım izleri yakınlaştırınca boncuklanıyor | Kuyruklar çok ince | En az yaklaşık 1.25 px genişlik; kuyruğu daha az inceltin (8.4) |
| Bir kıvılcım jetin içinden görünüyor | Jete karşı derinlik testi yok | Jetin arkasında derinliğe göre söndürün (8.4) |
| Bir akıntının çevresinde bir hale kâğıdı lekeliyor | Boş havada koyu tema bloom'u | Açık temada havada hale yok (8.3) |
| Buharda somon renkli bir leke | Bulutun üzerine yatırılmış parlama mürekkebi | Bulutu soluk kehribara doğru renklendirin (8.4) |
| Plazma açıkken kap gri okunuyor | Renk tonu çok hızlı düşüyor | Uzun menzilli bir yıkama terimi (8.3) |
| Bir kenar boyunca soluk bir saçak ya da beyaz bir dikiş | Mürekkep konturdan önce duruyor ya da konturlanmamış bir kıvrımı temizliyor | Mürekkebi konturun iç kenarında bitirin; kıvrımı mürekkeple çizin (8.6) |
| Jette krem renkli bir yama | Bir kopya diğer kopyanın terimini alıyor | Her kopyayı kapılayın (8.2) |
| Koyu kartın yedeği görünmüyor | Bir :not([data-theme="dark"]) kuralı açık sayfa üzerinden eşleşti |
Koyu kuralı en sona sıralayın ya da karta bağlayın (8.5) |
| Gri, solgun bir yedek parlama | Soluk bir belirteç üzerinde opaklık | Doygun belirteçler; multiply renk tonları, normal soluk çekirdekler (8.5) |
| Sıcak bir yüzün içinden gri çizgiler | Yarı saydam bir mürekkebin altında boşluklu SVG konturları | Yüzü doldurun (8.5) |
| Kaynağın göremediği bir yüzde ışık | Gerçek örtücüsüz bir yüzey ışığı | SVG'nin çizdiği aynı geometriyi kullanın (8.3) |
8b. Dönen bir gruptaki shader'lar
3B bir şekilde (references/3d.md) parçalar rijit gruplar olarak hareket eder; bu yüzden bir shader onları izlemek zorundadır.
- Bir parçaya bağlayın:
controller.anchor(group, restPoint, restDir)her karede{ world, screen, direction }verir. Bunları uniform olarak verin. Bir LED bükülen bir parmak ucunda böyle gezer, bir alev kopmuş bir hattın ucunda böyle kalır ve bir kıvılcım kaynağı bir mafsalda böyle durur. - Bir yüzeye bağlayın:
glslPose("Dial")'ı (ya da tek bir grup içinGLSL_TURN'ü) başa ekleyin veTURN.poseUniforms(controller, "dial", "uDial")'ı verin. Sonravec3 q = toDial(onFloor(vb, z))grubun dinlenme-dünyası koordinatlarındaki yüzey noktasıdır. Bir döner tablada biriken yağ, ışıkla ortaya çıkan bir doku ve bir yanık izi hepsi onunla döner. - Dünya uzayı dünya uzayı olarak kalır. Duman, kaynaktan ayrıldıktan sonraki kıvılcımlar ve tezgâhtaki yağ asla bir grup dönüşümüne dokunmaz. Çapanın dünya konumunda yayın, sonra dünyada entegre edin.
- Canlı parçalarla örtme. Ön tuval canlı bir katmanın üzerinde durur. Mürekkebini efektin kendi parçasından sonra boyanan parçalarla maskeleyin:
TURN.cover(controller, [partName])→uCoverEdge,uCoverCount, sonraink *= 1. - coverOf(vb, .7 * px). Maskeler 0.3 vb dışa itilmiş tam dışbükey siluetlerdir; böylece mürekkep örtenin konturunda durur. Her biri on kenarlı altı çokgen.MAX_FRAGMENT_UNIFORM_VECTORS'ü okuyun ve SVG yedeğini 128'in altında tutun. - Canlı katmanlar arasındaki bir arka tuval maske gerektirmez: ışık alan yüzeyi onu örten parçaların altındaki kendi canlı katmanına koyun (dönen kadranın yüzü elin altında).
- Vektör uniform dizileri (
uniform vec3 uSpark[24])drawüzerinden birFloat32Arrayalır. - Hareketli bir parçadaki SVG yedeği bir
T.billboard({ part, at, normal, svg })'dur. İşaretlemesi dünya birimlerinde değil, izdüşürülmüş noktada ortalanmış viewBox px cinsindendir; bu yüzden dünya boyutlarınıG.scaleOf(P)ile ölçekleyin: 1.1 yarıçaplı bir mercek için boyutlandırılmış bir parlama<circle r="${(1.1 * G.scaleOf(P)).toFixed(2)}"/>'dir.r="1.1"olarak yazılırsa merceğin boyutunun yaklaşık üçte biri kadar bir iğne noktası çizer. Bunu--no-webglve 4× yakınlaştırmayla kontrol edin.
9. Doğrulama
Hareket eden her şey için scripts/drive.mjs kullanın; capture.mjs'te artık WebGL de vardır, bu yüzden bir shader sayfasının durağan görüntüleri için uygundur. Kullanımı references/verify.md içerir. Normal kontrol listesinin üzerine:
- Her geçiş boyunca bir temas sayfası: başlangıç, her evre değişikliği, kararlı durum ve duruş. Ağır bir WebGL sayfasında gerçek zamanlı sayfalar fazla seyrektir, çünkü başsız bir yakalama 60–200 ms sürer ve animasyon karelerini engeller. Önce sayfanın saatini
["slow", 0.05]ile yavaşlatın (requestAnimationFrameveperformance.nowiçin 20× ağır çekim), sayfayı alın, sonra["slow", 1]. İlk karede başlayan bir şeyi, örneğin ilk ışığın söner gelişini yakalamak için, sayfa ilk karesinden yavaş çalışsın diyewindow.__driveSpeed=0.04içeren bir dosyayı--preloadedin. Bir evreyi tahmin edilmiş birwaityerine["until", "document.querySelector('[data-readout]').textContent.includes('main stage')", 8000]ile bekleyin. Her sayfadan önce boşta turu durdurun: devam zamanlayıcısı yavaşlatılmış saatle değil duvar saatiyle çalışır; bu yüzden sayfanın ortasında hedefleri geri alabilir. Bir tuş basışı onu birkaç gerçek saniye tutar; uzun bir yavaşlatılmış sayfa için zamanlayıcıyı--preloadile saplayın ve önce dinlenmeyi bekleyin (verify.md, "Yavaş mod zamanlayıcıları yavaşlatmaz"). - Yalnızca resimler değil, sayılar.
canvas.getContext("webgl")sayfanın kendi bağlamını döndürür. BirrequestAnimationFramegeri çağrısında uniform'larıgl.getUniform(gl.getParameter(gl.CURRENT_PROGRAM), location)ile okuyun ve ortalama tuval RGB'sinigl.readPixelsile okuyun. Geri okuma yalnızca çizimle aynı görevde çalışır, çünküpreserveDrawingBufferkapalıdır: ayrı bir görevden gelen tümü sıfır kareler bir test yapaylığıdır. Tüm bir çalıştırma boyunca kare başına bir satır kaydedin. Renk tonu ve ortalama parlaklık, kasıtlı patlamalar dışında, bir evre değişikliği boyunca her biri monoton olarak hareket etmelidir. - Kenarlar 0 okur. Her evre sırasında tuvalin son 10 satırını ve sütununu geri okuyun. 0'ın üstündeki her şey screen karışımı altında görünecek bir kesimdir.
- Açık temada parlaklığı değil, mürekkebi kaydedin. Kare başına bir bölgenin beyaz üzerine bıraktığı mürekkebi,
1 − lum(rgb + 1 − a),verify.md'deki kancayla ("Açık tema shader'ları") kaydedin. Bunu önemli her bölge (bir boşluk, çekirdek, yalnızca yedeğin kapladığı bir alan) için ilk ışık, her evre değişikliği ve sönme boyunca yapın. Mürekkep fiziksel niceliği izlemelidir: ışık gelirken artar, giderken azalır, tümsek ve çukur olmadan. Sayılarla söylenirse, art yakıcının kesim penceresi 1894 → 155 düştü ve ışık kapanışı 154 → 1959 yükseldi ve 1914'te oturdu. - Açık temada fiziksel rampa boyunca renkleri yoklayın. Isınan ya da soğuyan bir şey için merkez ve kenar pikselini birkaç sıcaklıkta okuyun. Mat kırmızı → turuncu → sarı → beyaza yakın ilerlemeli, kenar merkezden serin olmalı ve somon ya da gri bir aşama olmamalıdır. Cam fırını ağzının tablosu 930 °C'de merkezde 255/179/99 ve kenarda 252/114/25'ten, yaklaşık 620 °C'de 211/63/6 ve 165/28/1 üzerinden, 465 °C'ye kadar saydam cama ilerler. Bir hacim için kenardan kenara bir mürekkep kesiti okuyun: eksende zirve yapmalıdır.
- Açık temada koyu yolu da kontrol edin. Her açık tema değişikliği
uLight'ın arkasında durur; bu yüzden koyu derlemenin kareleri taban çizgisiyle eşleşmelidir. Ayrıca açık bir sayfanın içinde koyu bir kart çizin, WebGL'li ve WebGL'siz (iç içe tema tuzağı, bölüm 8.5). - Efektin SVG ile buluştuğu her yerin 2× ve 4× kırpımları: kenarlar, dudaklar, platform kenarları, duvar bantları, kâğıt köşeleri, örtücüler ve efektin içinden görüldüğü her açıklık (dört tarafta da kenarda duruyor mu, altta girinti duvarı görünüyor mu?). Dikişler, sert kenarlar, bantlaşma, koyu bir yaka, çift bir çizgi ya da ani bir renk değişimi arayın. Açık temada ayrıca mürekkebin bir konturla buluştuğu her yerde (kesik yüzler, kenarlar, ağızlar, kıvrımlar) 8–16× kırpımlar çekin ve mürekkep ile çizgi arasında soluk bir boşluk, bir çizginin üzerine boyanmış mürekkep ya da mürekkebin temizlediği bir kontur arayın. Aynı kırpımı ön SVG gizliyken ve tuval gizliyken karşılaştırın (bir
evalilestyle.display="none"ayarlayın). - Yedek.
drive.mjs'i--no-webglile çalıştırın (hergetContext("webgl")nulldöndürür) vedata-gl'nin olmadığını ve yalnızca SVG'nin iki temada da hâlâ okunduğunu kontrol edin. Açık temada yedek shader'la aynı mürekkepleri ve aynı koyu zemini göstermeli ve shader'ın çizdiği her gövde orada olmalıdır. İlk ışığı, sayfayı ilk karesinden yavaşlatan--preloadile bir sayfada filme alın: yedekten shader'a devir çukura düşmemelidir (bölüm 8.5). - Azaltılmış hareket (
--reduced): tek, temsil edici bir durağan kare. - Konsol temiz. Bir shader derleme hatası
onError'dan birconsole.errorolarak gelir.
10. Performans
- Maliyet kabaca pikseller × örneklerdir. Raptor her biri dört şok hücresi terimli 28 hacim örneği, artı 7 ikiye bölmeli 28 adımlık bir zemin araması, 8 örnekli bir hale, 10 örnekli bir sıçrama perdesi ve tuval başına 6 buhar örneği çalıştırır; 1.5× cihaz oranına kadar. SwiftShader'da çalışır, ama kare süresi gerçek bir GPU'da ölçülmemiştir. Daha ağır bir şey yayınlamadan önce gerçek bir tarayıcının Performance panelinde ölçün ve çok yavaşsa önce örnekleri, sonra
maxRatio'yu kesin. - Dalga leğeninin kâğıt shader'ı iki kaynaklı alanı piksel başına dört kez değerlendirir (kırılma kayması için bir kez ve kostiğin sabit nokta tersi için üç kez); her kaynak 24 adımlık bir uniform döngüsü yerine geçmişinin altı doku okumasına mal olur. Bir dizüstü bilgisayar GPU'sunda çizim başına yaklaşık 1 ms ölçüldü; telefon maliyeti ölçülmedi.
- Hangi GPU'yu ölçtüğünüzü bilin.
drive.mjsvecapture.mjs'teki başsız Chrome yapabildiğinde gerçek GPU'yu, aksi halde SwiftShader'ı kullanır; bir kare süresine güvenmeden öncegl.getParameter(gl.RENDERER)'ı kaydedin. - Örnekleri yalnızca önemli oldukları yere harcayın. Yürütmeyi sınırlayın (bir silindir ya da kutu), zemin isabetinde başlayın ve tüm terimleri uniform'larla kapılayın.
- İki tuval iki tam ekran geçişe mal olur. Gösterecek bir şeyi olmayanı temizleyin (Raptor'un örtüsü yalnızca buhar, buğu ya da şok varken çizilir).
- Alandaki hiçbir şeyin ihtiyacı yoksa
uTime'ı canlandırmayın. Dalga leğenidraw(values, 0)verir ve yalnızca dalga fazını hareket ettirir; böylece azaltılmış hareket karesi gerçekten durağandır. - Boştayken döngüyü durdurun. Hiçbir şey olmazken 60 fps'de kalan bir shader sayfası bir hatadır.