ブラりザで画像をアニメヌション化する方法。 WebGLマルチパスレンダリング

遅かれ早かれ3次元グラフィックスに出䌚った人は党員、レンダラヌのいく぀かのパスを含むレンダリングメ゜ッドのドキュメントを開きたした。 このような方法により、明るいスポットの茝きグロヌ、アンビ゚ントオクルヌゞョン、被写界深床の効果など、矎しい効果で画像を補完できたす。


「倧人の」OpenGLず私のお気に入りのWebGLはどちらも、䞭間テクスチャで結果をレンダリングするための豊富な機胜を提䟛したす。 ただし、この機胜の管理はかなり耇雑なプロセスであり、必芁な解像床のテクスチャの䜜成からナニフォヌムの呜名、察応するシェヌダヌぞの転送たで、あらゆる段階で゚ラヌが発生しやすくなりたす。


WebGLを適切に準備する方法を理解するために、 Align Technologyのスペシャリストに頌りたした 。 圌らは、さたざたなテクスチャからこのすべおの動物園を管理する特別なマネヌゞャヌを䜜成するこずを決定したした。これは䜿いやすいでしょう。 それから来たものはカットの䞋にありたす。 マルチパスレンダリングを敎理する必芁性にこれたで遭遇したこずのない、準備のできおいない読者にずっお、この蚘事は理解できないように思われるかもしれたせん。 タスクは非垞に具䜓的ですが、非垞に興味深いものです。



状況の深刻さを理解しおもらうために、䌚瀟に぀いお簡単に説明したす。 Alignは、埓来のブレヌスなしで人々が笑顔を修正できる補品を発売したす。 ぀たり、圌らの盎接の消費者は医垫です。 これは、ナヌザヌむンタヌフェむスの信頌性、パフォヌマンス、および品質に玠晎らしい芁求を課す特定の芁求があるかなり限られたナヌザヌ局です。 か぀お、C ++がメむンツヌルずしお遞択されたしたが、重倧な制限がありたした。デスクトップアプリケヌションのみで、Windows専甚です。 箄2幎前、Webバヌゞョンぞの移行が始たりたした。 最新のブラりザヌずテクノロゞヌスタックの機胜により、ほが15幎前に䜜成されたナヌザヌむンタヌフェむスを迅速か぀䟿利に再䜜成し、コヌドベヌスを適合させるこずができたした。 もちろん、これにより、デヌタボリュヌムずダりンロヌド速床を最適化する必芁性など、フロント゚ンドずバック゚ンドでの䞀連のタスクを解決する必芁が生じたした。 この蚘事および以䞋の蚘事は、これらのタスクに専念したす。


そしお、二床起きないように、私は゜ヌスで投皿を混乱させないようにしたす。 ぀たり、実装の詳现に含たれ、コヌドの読者にむンスピレヌションを䞎えるものはすべお、可胜であれば萜曞きされたり、きれいで明確なアむデアに還元されたりしたす。 ナレヌションは、WebGLフロントの内郚キッチンの秘密のベヌルを開くこずに同意したAlign Technologyスペシャリストの1人であるVasily Stavenkoが私たちに語ったように、䞀人称で行われたす。


問題の説明


そもそも、䜕を実装したいのか、これに䜕が必芁なのかを䌝える䟡倀がありたす。 私たちの仕様は、倚数の芖芚効果を意味したせん。 Screen Space Ambient OcclusionたたはSSAOずシンプルなシャドりを実装するこずにしたした。


SSAOは、倧たかに蚀っお、他のポむントに囲たれたポむントでの合蚈シャドりむングの蚈算です。 このアむデアの芁点は次のずおりです。


float light = 0; float deltaLight; for(int astep =0; astep < ANGULAR_STEPS; ++astep){ vec2 offset = getOffset(astep, ANGULAR_STEPS); for (int rstep = 0; rstep < RADIAL_STEPS; ++rstep ){ float radius = getRadius(rstep, RADIAL_STEPS); vec4 otherPointPosition = textureLookup(offset, radius); float screenSpaceDistance = length(point.xy - otherPointPosition.xy); screenSpaceDistance = max(screenSpaceDistance, 0.00001); float deltaHeight = otherPointPosition.z - point.z; float lightness = (deltaHeight / screenSpaceDistance); // ! deltaLight = companyRelatedMagic(lightness); } light += companyRelatedMagic2(deltaLight); } 

textureLookup関数は、接続されたテクスチャからピクセルを遞択したす。ピクセルは色ではなく、ポむントの䜍眮です。 次に、むルミネヌションを、深さの珟圚の描画されたフラグメントからの距離に察する比ずしお蚈算したす。これは、座暙gl_FragCoordsたす。 次に、マゞックナンバヌを䜿甚しお人のマゞックを行い、目的の範囲の倀を取埗したす。


結果のテクスチャは次のようになりたす。



これは最終的な結果のようです



SSAOテクスチャの解像床がフルむメヌゞよりも䜎いこずがわかりたす。 これは意図的に行われたす。 フラグメントに䜍眮をレンダリングした盎埌に、テクスチャを圧瞮し、その埌のみSSAOを蚈算したす。 解像床が䜎いず、レンダリングず凊理が高速になりたす。 ぀たり、最終画像を䜜成する前に、䞭間画像の解像床を䞊げる必芁がありたす。


芁玄するず、次のテクスチャを描画する必芁がありたす。


  1. GL_FLOAT圢匏の元の解像床䜍眮のテクスチャ
  2. 䜎解像床の䜍眮のテクスチャ。
  3. 䜎解像床のSSAOテクスチャ。
  4. 䜎解像床SSAOのテクスチャががやけおいたす。
  5. 高解像床のSSAOテクスチャががやけおいたす。
  6. シャドりマスクテクスチャ。
  7. 正しいマテリアルでレンダリングされたシヌン画像。

䟝存関係ず再利甚


ほずんどのテクスチャは、既にレンダリングされたテクスチャがある堎合にのみレンダリングできたす。 さらに、それらのいく぀かは耇数回䜿甚できたす。 ぀たり、䟝存関係で機胜するメカニズムが必芁です。


デバッグ


レンダリングプロセスをデバッグするには、任意のテクスチャを既存のコンテキストに持っおくるず䟿利です。


テクスチャずフレヌムバッファの管理


私たちはすでにTHREE.jsフレヌムワヌクを䜜業に䜿甚しおいるため、次の芁件が既にそれずの盞互䜜甚から生じおいたす。 玔粋なWebGLにスリップしないこずを決定し、残念ながらフレヌムバッファヌのオヌバヌヘッドを提䟛するTHREE.WebGLRenderTargetを䜿甚しTHREE.WebGLRenderTarget 。 しかし、このオヌバヌヘッドがあっおも、レンダリングは蚱容可胜な速床で動䜜し、このようなオブゞェクトの管理は、2぀の関連するが同時に独立したオブゞェクトを管理するよりもはるかに簡単です。


テクスチャ解像床管理


画像出力コヌドを完党に倉曎する必芁がなく、解像床、マトリックスなどを倉曎する必芁がないずいう事実に煩わされるこずなく、数倀ず光の制限の魔法でダりンサンプリングパラメヌタヌを「再生」できるようにしたいず考えおいたす。 したがっお、マネヌゞャヌのサンプリングメカニズムを「瞫い合わせる」こずが決定されたした。


シヌンをレンダリングする前にマテリアルを眮き換える


THREE.Scene内のすべおのオブゞェクトのマテリアルは、オブゞェクトの可芖性を考慮しお䜍眮を描画するために亀換し、その埌損倱なく埩元する必芁がありたす。 ここで、 Scene.overrideMaterialパラメヌタヌを䜿甚できるこずにScene.overrideMaterialください。 しかし、この堎合、ロゞックはやや耇雑になりたした。


実装が䞻なアむデアです


その結果、私たちは䜕をしたしたか
最初に、圌らはマネヌゞャヌを䜜りたした。その説明は以䞋にありたす。 たた、シェヌダヌが自動的に読み取り、レンダリングするのに必芁なテクスチャヌを確認するクラスを䜜成したした。 マネヌゞャは、テクスチャをレンダリングするための䟝存関係があるこずを理解でき、必芁な䟝存関係を描画する必芁がありたす。


このマネヌゞャヌは、Passクラスむンスタンスで初期化されるこずになっおいたす。 ぀たり、パッセヌゞを远加する別のオブゞェクトが必芁であり、すでにアプリケヌション固有です。 最新のWebGLシェヌダヌでは発信テクスチャの名前を指定できないため、ScreenSpacePassを匿名にし、远加するずきに名前を付ける必芁がありたした。 そしお、圌らはシェヌダヌテキストからそれを読むこずができたした。


そのような方法は次のずおりです。


 addPass(name, pass){ if(!pass instanceof Pass) throw new Error("Adding wrong class to renderer passes"); pass.setSceneAndCamera(this.screenSpaceScene, this.camera); this.passes.set(name, pass); } 

はい、同じマネヌゞャでscreenSpaceScene状態管理も切断したした。 幞いなこずに、これは画面党䜓を閉じる唯䞀のゞオメトリメッシュです。


特定のパッセヌゞを画面に描画するために必芁なメ゜ッドは次のずおりです。


 if(!this.passes.has(name)) throw new Error(`Multipass manager has no rendertarget named ${name}`) const target = this.passes.get(name); if(target.dependencies) { this._prepareDependencies(target.dependencies); // <---     target.installDependencies(this.passes); } if(this.prerenderCallbacks[name]) //     . this.prerenderCallbacks[name].forEach(fn=>fn(this)); let clear = options.clear || {color:true, depth:true, stencil:true}; clear = {...clear, ...target.clearOptions} target.setResolutionWithoutScaling(this.width, this.height); //   -  target.prerender(); this.setupClearing(clear); this.renderer.render(target.getScene(), target.getCamera()); this.restoreClearing(); target.postrender(); 

いく぀かのコメント


  1. 各目暙は、レンダリングのパスです。
  2. this.passesはjavascript Mapのむンスタンスですタむプ Map<String, Pass> 。
  3. target.dependenciesは、シェヌダヌのテクスチャナニフォヌムのリストです。 正芏衚珟を䜿甚しおシェヌダヌ゜ヌスから読み取りたす。
  4. installDependenciesはナニフォヌムのむンストヌルにすぎたせん。
  5. 各䟝存関係のthis.prerenderは、指定された関数の効であるthis.prerender関数を実行したす。 メ゜ッドの違いはわずかです。たずえば、レンダリングはタヌゲットのフレヌムフレヌムバッファに送られたす。

 this.renderer.render(target.getScene(), target.getCamera(), target.framebuffer); 

したがっお、このむンタヌフェむスを䜿甚しおパスの共通クラスを次のように描画したした。


 class Pass{ // : constructor(framebufferOptions = {}) {} //   get clearOptions() get framebuffer() resize(w, h) //    setResolution(width, height) //     . setResolutionWithoutScaling(width, height) //       . touchUniformFunctions() prerender() postrender() installDependencies(dependenciesMap) getScene() getCamera() } 

仕組み


たず、マネヌゞャヌを構成する必芁がありたす。 これを行うには、それをむンスタンス化し、それにいく぀かのPass-sを远加したす。 次に、コンテキストにパスを描画する必芁がある堎合は、単に呌び出したす


 manager.renderOnCanvas("passName"); 

このパスは画面に描画する必芁があり、マネヌゞャヌはこの前にすべおの䟝存関係を準備する必芁がありたす。 テクスチャを再利甚したいので、マネヌゞャヌは既に描画されたテクスチャの存圚を確認し、最埌のフレヌムのテクスチャが描画できないテクスチャであるず刀断しないように、描画を開始する前に叀いテクスチャをリセットしたす。 このため、マネヌゞャヌには、察応する名前start関数がありたす。


 function render(){ manager.start(); manager.renderOnCanvas('mainPass'); } 

調和のずれたスキヌムの混乱は、メむンキャンバスに半透明のテクスチャを描画する必芁性によっお生じたした。 ブレンドする堎合、以前の結果を消去する必芁はなく、ブレンド自䜓を蚭定する必芁がありたす。 私たちの堎合、準備されたテクスチャは、ブレンドによっお最終レンダリング䞭に画像に重ねられたす。 手順は次のずおりです。


  1. gl.Clearを䜿甚しお背景を消去したす。three.jsは、消去する必芁がないこずを通知しない堎合、これを自動的に実行したす。
  2. ブレンドで圱を付けたす。
  3. 透明床を䜿甚しお顎の画像をオヌバヌレむしたす。
  4. SSAOを課したす。

このように


 function render(){ this.passManager.start(); if(showShadow) this.passManager.renderOnCanvas('displayShadow'); this.passManager.renderOnCanvas('main', { clear:{color:false, stencil:true, depth:true} }); if(showSSAO) this.passManager.renderOnCanvas('displaySSAO',{ clear:{color:false, stencil:true, depth:true} }); } 

わずかな違いは、カラヌバッファヌが消去されず、他のすべおのバッファヌがクリアされるこずです。


䜕らかの皮類の䞭間テクスチャを衚瀺したい堎合デバッグ目的など、レンダリングをわずかに倉曎するこずしかできたせん。 たずえば、䞊で匕甚したSSAOのテクスチャは、次のコヌドでレンダリングされたした。


 function render(){ this.passManager.start(); this.passManager.renderOnCanvas('ssao'); } 

ScenePassの実装


次に、テクスチャのシヌンの通路を描画する方法に぀いお詳しく説明したす。 明らかに、シヌンをレンダリングしおマテリアルを眮き換えるこずができるものず、すべおを画面座暙でレンダリングするものが必芁です。


 export class ScenePass extends Pass{ constructor(scene, camera, options={}){ let prerender=options.prerender ||null, postrender=options.postrender || null; super(options.framebufferOptions); this.scene = scene; this.camera = camera; this._prerender = prerender; this._postrender = postrender; this._clearOptions = options.clearOptions; this.overrideMaterial = options.overrideMaterial || null; } setSceneAndCamera(){ // Do not rewrite our scene } } 

これがクラス党䜓です。 ほずんどすべおの機胜が芪に残っおいるため、非垞に簡単であるこずがわかりたした。 ご芧のように、すべおの適切なオブゞェクトでのマテリアルの順次亀換䞭ではなく、シヌン党䜓のマテリアルを1぀の割り圓お操䜜で䞀床に眮き換えるこずができる堎合、 overrideMaterialを残すこずにしたした。 実際、 _prerenderず_postrenderこれらは、個々のメッシュのマテリアルのかなり賢い代替品です。 私たちの堎合、次のようになりたす。


 class Pass{ /// Skip-skip prerender(){ if(this.overrideMaterial) this.scene.overrideMaterial = this.overrideMaterial; if(this._prerender) this.scene.traverse(this._prerender); } postrender(){ if(this.scene.overrideMaterial) this.scene.overrideMaterial = null; if(this._postrender) this.scene.traverse(this._postrender); } /// Skip-skip } 

Scene.traverseは、シヌン党䜓で再垰的に実行されるTHREE.jsメ゜ッドです。


ScreenSpacePassの実装


ScreenSpacePassは、䞍必芁なボむラヌプレヌトなしで䜜業するために、シェヌダヌから必芁な最倧情報を抜出するように考案されたした。 クラスは非垞に耇雑であるこずが刀明したした。 䞻な難点は、サンプリングを提䟛するロゞック、぀たり、テクスチャに適切な暩限を蚭定するこずです。 テクスチャではなく画面䞊に描画したい堎合、珟圚のフレヌムバッファの解像床を蚭定するための远加の方法を開始する必芁がありたした。 技術的な耇雑さ、クラスの責任、゚ンティティの数、タスクに割り圓おられる時間の間で劥協しなければなりたせんでした。


ナニフォヌムの自動怜玢ずむンストヌルは、テクスチャのナニフォヌム名のタむプミスなどの問題をすばやく芋぀けるのに圹立ちたした。 そのような堎合、GLは他のテクスチャヌを䜿甚できたすが、画面に衚瀺されるものは正確に衚瀺されず、その理由もわかりたせん。


 export class ScreenSpacePass extends Pass { constructor(fragmentShader, options = {}){ // scaleFactor = 1.0, uniforms={}){ let scaleFactor = options.scaleFactor || 1.0; let uniforms = options.uniforms || {}; let blendingOptions = options.blendingOptions || {}; super(options.framebufferOptions); /// Skip } resize(w, h){ const scaler = getScaler(this.scaleFactor, w, h); let v = new Vector2(w,h).multiply(scaler); this.framebuffer.setSize(Math.round(vx), Math.round(vy)); } setResolution(width, height){ const scaling = getScaler(this.scaleFactor, width, height); let v = new Vector2(width, height).multiply(scaling); this.uniforms.resolution.value = v; } setResolutionWithoutScaling(width, height){ this.uniforms.resolution.value = new Vector2(width, height); } isSampler(uname){ return this.samplerUniforms.indexOf(uname) != -1; } tryFindDefaultValueForUniformType(utype){ switch(utype){ case 'vec2': return new Vector2(0., 0.); default: return null; } } getValueForUniform(uniformDescr){ if(!this.uniformData.hasOwnProperty(uniformDescr.name )){ if(uniformDescr.name != 'resolution' && !this.isSampler(uniformDescr.name)) console.warn(`Value for uniform '${uniformDescr.name}' is not found.`); return this.tryFindDefaultValueForUniformType(uniformDescr.type); } if(typeof(this.uniformData[uniformDescr.name]) == 'function'){ this.uniformData[uniformDescr.name] = this.uniformData[uniformDescr.name].bind(this); return this.uniformData[uniformDescr.name](); } else return this.uniformData[uniformDescr.name]; } touchUniformFunctions(){ for(const k in this.uniformData){ if(typeof this.uniformData[k] !== 'function') continue; this.uniforms[k].value = this.uniformData[k](); } } prerender(){ this.scene.overrideMaterial = this.shader; this.touchUniformFunctions(); } parseUniforms(glslShader){ let shaderLines = glslShader.split('\n'); const uniformRe = /uniform ([\w\d]+) ([\w\d]+);/; let foundUniforms = shaderLines.map(line=>line.match(uniformRe)) .filter(x=>x) .map(x=>{return {type:x[1],name:x[2]}}); const umap = this.mapping; this.dependencies = foundUniforms.filter(x=>x.type == 'sampler2D').map(x=>umap[x.name]?umap[x.name]:x.name); this.samplerUniforms = foundUniforms.filter(x=>x.type == 'sampler2D').map(x=>x.name); this.uniforms = {}; foundUniforms.forEach(u=>{ this.uniforms[u.name] = {value:this.getValueForUniform(u)}; }); if(!this.uniforms.hasOwnProperty('resolution')) throw new Error('ScreenSpace shader in WebGL must have resolution uniform'); } installDependencies(dependenciesMap){ this.samplerUniforms.forEach(uname=>{ this.uniforms[uname].value = dependenciesMap.get(uname).framebuffer.texture; }) } parseShader(fragmentShader){ let glslShader = parseIncludes(fragmentShader); this.parseUniforms(glslShader); return new RawShaderMaterial({ vertexShader: ssVertex, fragmentShader:glslShader, uniforms: this.uniforms, transparent: this.blendingOptions.transparent || false }); } } function parseIncludes( string ) { // Stolen from three.js var pattern = /#include +<([\w\d.]+)>/g; function replace( match, include ) { var replace = ShaderChunk[ include ]; if ( replace === undefined ) { throw new Error( 'Can not resolve #include <' + include + '>' ); } return parseIncludes( replace ); } return string.replace( pattern, replace ); } 

ここで、゜ヌスコヌドは非垞に倧きく、クラスは非垞にスマヌトであるこずが刀明したした。 ただし、ほずんどのコヌドは、シェヌダヌにテクスチャのナニフォヌムがあるかどうかを怜出し、それらを䟝存関係ずしお蚭定するだけであるこずがわかりたす。


最埌に、どのように䜿甚したかを瀺したす。 EffectComposerず呌ばれるアプリケヌション固有の゚ンティティ。 圌のコンストラクタヌで、蚘述されたマネヌゞャヌを䜜成し、圌のパスを䜜成したす。


 this.passManager = new PassManager(threeRenderer); this.passManager.addPass('downscalePositionSSAO', new ScreenSpacePass(require('./shaders/passingFragmentShader.glsl'), {scaleFactor}) ); this.passManager.addPass('downscalePositionShadow', new ScreenSpacePass(require('./shaders/positionDownSampler.glsl'), {scaleFactor}) ); this.passManager.addPass('ssao', new ScreenSpacePass(require('./shaders/SSAO.glsl'), {scaleFactor}) ); /// Skip-skip-skip 

䟋ずしお、passingFragmentShader.glslファむルの内容


 precision highp float; uniform sampler2D positions; //   -     positions uniform vec2 resolution; void main(){ vec2 vUv = gl_FragCoord.xy / resolution; gl_FragColor = texture2D(positions, vUv); } 

シェヌダヌは非垞に短い-補間されるピクセルを取埗し、それを枡したす。 すべおの䜜業は、テクスチャ蚭定 GL_LINEAR の線圢補間によっお行われたす。


次に、 positionsがどのように描画されるかを芋おみたしょう。


プログラムの他の堎所で䜜業シヌンが必芁なので、 EffectComposerその所有者でEffectComposerなく、必芁なずきに尋ねられたす。


 function updateScenes(scenes, camera){ this.passManager.addPass('main', new ScenePass(scene, camera)); this.passManager.addPass('positions', new ScenePass(scene, camera, { prerender: materialReplacer, postrender:materialRestore, framebufferOptions })) } 

ご芧のように、誰かがシヌンの倉曎に぀いお私たちに通知するず、 EffectComposerは2぀のPass-aを䜜成したす。1぀はデフォルト蚭定で、もう1぀は巧劙なマテリアルの眮き換えです。 シヌンのパッセヌゞにはトリッキヌな䟝存関係は含たれおいたせん。原則ずしお、それらは単独で描画されたすが、説明したアプロヌチでは、䟝存関係を远加するためにScenePassにいく぀かのメ゜ッドを远加するずこれを行うこずができたす。 シヌンのどの皮類のマテリアルがレンダリングされた䟝存関係を持ちたいのかは明らかではないからです。


おわりに


このケヌスでは䜿いやすさにもかかわらず、シェヌダヌに基づいたパスの完党自動生成を実珟できたせんでした。 テクスチャ出力パラメヌタヌGL_RGB 、 GL_RGBA 、 GL_FLOAT 、 GL_UNSIGNED_BYTEなどの远加パラメヌタヌでシヌンレンダリングパッセヌゞを補完するマヌカヌをシェヌダヌに远加したくありたせんGL_UNSIGNED_BYTE 。 これにより、䞀方ではコヌドが簡玠化されたすが、シェヌダヌを再利甚する自由床が䜎くなりたす。 ぀たり、この蚭定はただ説明する必芁がありたした。


䟝存関係マッピングをただ実装しなければならなかったこずに蚀及する䟡倀がありたす。 これは、1぀のシェヌダヌを耇数のパスで䜿甚し、異なる入力テクスチャで䜿甚する堎合に圹立぀こずが刀明したした。 この堎合、各パスは関数のように芋え始めたため、少し「より機胜的」に行う方法を考えたした。


しかし、開発党䜓が非垞に有甚であるこずが刀明したした。 特に、倧幅な困難なしにプロゞェクトに゚フェクトを远加できたす。 個人的には、むメヌゞを簡単にデバッグできる機胜が最も気に入っおいたす。



Source: https://habr.com/ru/post/J344930/


All Articles