スクラッチ部分9からWebGL 3 Dエンジン:頂点照明+カメラズーム


前回、我々はポリとファセット照明ごとに見た.今回は、私たちは、1ピクセルあたりのピクセル照明の間に来たステップを見ていきます.

材料のリファクタリング
物事は少しハード対処するために取得し始めている.遮光物の各々の変化はコード修正を必要とします、そして、私は私自身がものを内外でコメントするのを見つけます.私たちがそれらの概念を抽象化するならば、何がよりよいでしょうか.そうするために、我々は「物質」と呼ばれる新しい実体をつくります材料が本当に「俳優」でないので、私はそのフォルダをentity . また、新しいフォルダを作成しますshaders GLSLコードを再利用するために収容します.大会は{name}.vert.glsl or {name}.frag.glsl 遮光物の種類を区別する.
ここで指摘する1つの点は、ファイルに対する抽出がネットワーク要求を作る必要があるときにロードを遅くするということです.理想的には、これらを束ねるか、インラインにすることができるいくつかの種類の構築システムがあるでしょう.私はビルドツールを扱う必要はありませんが、生産アプリのためにする必要はありませんので、いつでもすぐにその最適化を見に行くことはありません.
インgl-helpers.js ネットワークから遮光物をロードして、プログラム全体をロードするために新しいユーティリティを追加することができます.
export function loadShader(context, url, type){
    return fetch(url)
        .then(r => r.text())
        .then(txt => compileShader(context, txt, type));
}

export async function loadProgram(context, url){
    const [vertexShader, fragmentShader] = await Promise.all([
        loadShader(context, `${url}.vert.glsl`, context.VERTEX_SHADER),
        loadShader(context, `${url}.frag.glsl`, context.FRAGMENT_SHADER)
    ]);
    return compileProgram(context, vertexShader, fragmentShader);
}
loadProgram ただベースパスを必要とし、両方の遮光物を読み込むことができます.それでは、いくつかのものをMaterial クラス
export class Material {
    #program;
    #textures;

    constructor(material){
        this.#program = material.program;
        this.#textures = material.textures ?? [];
    }

    get program(){
        return this.#program;
    }

    get textures(){
        return this.#textures;
    }
}
更新mesh.js 使用するクラスMaterial Aの代わりにtextureName (図示せず).bootGpu 今すぐ完全に取得することができますインラインの遮光物をファイルに移動することができます
async bootGpu() {
    this.context = this.dom.canvas.getContext("webgl2");
    this.context.enable(this.context.CULL_FACE);
    this.context.cullFace(this.context.BACK);
    //this.context.enable(this.context.DEPTH_TEST);
    this.context.clearColor(0, 0.5, 0.5, 1);
}
connectedCallback 直後に新しい方法を呼び出すことができますbootGpu :
async createMaterials(){
    this.materials = {
        flatShaded: new Material({
            program: await loadProgram(this.context, "shaders/flat-shaded")
        })
    };
}
メッシュを作るとき、材料を追加します.
createMeshes(){
    this.meshes = {
        sphere : new Mesh({
            ...facetSphere(20, { uvOffset: [0.5,0], color: [0.5,0.25,1]}),
            material: this.materials.flatShaded
        })
    };
}
そして最後にbindMaterial これは遮光物プログラムをアクティブにし、テクスチャをロードしますbindTexture ).
もはや世界的なプログラムがないので、我々はmesh.material.program 多くの異なるものに.この投稿は、すべての変更を表示するために読みにくいですが、私はあなたがそれを行うことができます信じたり、githubのコードを見てください.あなたがちょうどWebGL文脈を持っているならば、あなたは使うことができますcontext.getParameter(context.CURRENT_PROGRAM) 代わりに.
私も移動createTexture to gl-helpers.js そして、URLからテクスチャ全体をロードする新しいヘルパーを作成します.
export function loadTexture(context, url){
    return loadImage(url)
        .then(img => createTexture(context, img));
}
我々は今、いくつかのURLを変更することで完全に異なるシェーディングとテクスチャを使用できます.
async createMaterials(){
    this.materials = {
        flatShaded: new Material({
            program: await loadProgram(this.context, "shaders/textured"),
            textures: [await loadTexture(this.context, "./img/earth.png")]
        })
    };
}
また、これは非常に効率的ではないことに注意してください.同じ頂点遮光物が異なる断片遮光物のために有効であるならば、どうですか?我々は再び個々の部分にそれを壊すことができますが、私は今、これは罰金だと思う、私はとにかく単一のオブジェクトを行うだけです.

マルチクワッド面
私は平らな表面を構成するクワッドのコレクションである別のタイプのメッシュを追加したかった.これは、変形を見るときに後で役に立つでしょう.今のところ私はそれを呼び出すterrainMesh 私がより良い名前を見つけるまで、それは地形生成のために使われるでしょう.とにかく簡単に生成することができます.
export function terrianMesh(height, width){
    const positions = [];
    const colors = [];
    const uvs = [];
    const normals = [];
    const triangles = [];
    const startX = -(width / 2);
    const startZ = -(height / 2);

    let attributeIndex = 0;
    for(let row = 0; row < width; row++){
        for(let col = 0; col < height; col++){
            positions.push(
                    startX + col, 0, startZ + row,
                    startX + 1 + col, 0, startZ + row,
                    startX + 1 + col, 0, startZ + 1 + row,
                    startX + col, 0, startZ + 1 + row
            );
            colors.push(
                1, 0, 0,
                1, 0, 0,
                1, 0, 0,
                1, 0, 0
            );
            uvs.push(
                0, 1,
                1, 1,
                1, 0,
                0, 0
            );
            normals.push(
                0, 1, 0,
                0, 1, 0,
                0, 1, 0,
                0, 1, 0
            );
            triangles.push(
                attributeIndex, attributeIndex + 1, attributeIndex + 2,
                attributeIndex, attributeIndex + 2, attributeIndex + 3
            );
            attributeIndex += 4;
        }
    }

    return {
        positions,
        colors,
        uvs,
        normals,
        triangles,
        textureName: "grass"
    };
}
我々はどのように我々はクワッドグリッドをしたいし、ポリゴンを構築する方法を渡す.これはより大きな効率のためにポリストリップとしてされることもできました、しかし、私はメッシュが明白な三角形でどのように定義されるかについて、一貫してものを保っています.

あなたがゲームを作っているならば、これは床に対処するすばらしい方法です.

頂点lights
それで、一度は、小さなfancierゲームが頂点照明につき始めたので、多角形を一つの色に着色する代わりに、勾配でありえました.

信用https://www.marioboards.com/threads/38871/
スーパースマッシュブラザーズからこのマリオモデルを見てください.ここでは、それぞれの多角形が明らかに単色であるVirtuaファイタールックとは異なり、我々はいくつかの基本的なテクスチャーを見ることができます.これは頂点照明、またはそれ以外の“Gouraudシェーディング”として知られています.ここでは、各頂点での光の値を計算し、次に多角形を横切って線形補間します.その2番目の部分はおそらくベルを鳴らす、私たちは実際に無料でこの部分を取得し、それは私たちの以前の多色の形で起こっていたまさにそれです.
コードは非常に我々が前にあった一面ファセット照明に似ています.唯一の違いは、頂点間で共有された重心の相対的な照明を計算する代わりに、代わりに頂点位置を使用することです.
//vertex shader
uniform mat4 uProjectionMatrix;
uniform mat4 uModelMatrix;
uniform mat4 uViewMatrix;
uniform lowp mat4 uLight1;

attribute vec3 aVertexPosition;
attribute vec3 aVertexColor;
attribute vec2 aVertexUV;
attribute vec3 aVertexNormal;
varying mediump vec4 vColor;
varying mediump vec2 vUV;
varying mediump vec3 vNormal;
varying mediump vec3 vPosition;

varying mediump vec3 vToLight;
void main() {
    bool isPoint = uLight1[3][3] == 1.0;
    mediump vec3 normalNormal = normalize(vec3(uModelMatrix * vec4(aVertexNormal, 1.0)));
    mediump vec3 normalPosition = vec3(uModelMatrix * vec4(aVertexPosition, 1.0));

    if(isPoint) {
        mediump vec3 toLight = normalize(uLight1[0].xyz - normalPosition);
        mediump float light = dot(normalNormal, toLight);
        vColor = vec4(aVertexColor, 1.0) * uLight1[2] * vec4(light, light, light, 1);
        vToLight = toLight;
    } else {
        mediump float light = max(0.0, dot(normalNormal, uLight1[1].xyz));
        vColor = vec4(aVertexColor, 1.0) * uLight1[2] * vec4(light, light, light, 1);
    }

    gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aVertexPosition, 1.0);

    vUV = aVertexUV;
    vNormal = normalNormal;
    vPosition = normalPosition;
}
いくつかのエッジケースを説明するために少しクリーンアップしました.特にmax 照明の色が負になるのを防ぐ.フラグメント遮光物は全く同じです、色通過.
単純なquad ( 1 , 0 , - 1.5 )の点灯方法を以下に示します.

あなたが見ることができるように、それは右側から照らされます.何か他のことを試してみましょうuvSphere :

あなたは今人工物を見ることができます.多角形が少しchunkierであるとき、彼らは曲面に沿っての代わりに線形に内挿するので、あなたは端を見ることができます.それは明らかにそれが長い間人気があった理由であるより現実的です.また、我々の新しいTerrainメッシュを使用してそれを見ることができます.
( 0.5 , 0 , - 1 )の光

これは人工ファクトを本当に目立つようにします.縁は明るくなる傾向がある.古い3 Dゲームでこれを探してみてください!
前回はフラグメントごとに/ピクセルごとの照明を試みた(同じことがフラグメントシェーダで行われた).これを適用すると、現代のハードウェアは私たちをネットで見ることができます.

照明勾配は、多角形端の向こうでさえ滑らかに混ざります.

カメラズーム
私は実際にカメラをしたときに戻ってこの機能を追加することを忘れてしまったが、大きな多角形を見て近くに私は自分を再指向したいです.どのようにズームするか見てみましょう.イベントを登録することから始めます.
attachEvents() {
  this.dom.canvas.addEventListener("wheel", this.onWheel);
}
onWheel(e) {
    const delta = e.deltaY / 1000;
    this.cameras.default.orbitBy({ radius: delta });
}
ここであまりおもしろくない.メソッドにスケーリングされたデルタ値を渡すホイールイベントですzoomBy . 私のマウスの1つのスナップポイントは100単位に対応するので、1000を掛けることによって、我々は0.1の単位をオブジェクトに向かってまたは離れて動かします.私たちはzoomBy カメラで
orbitBy({ lat = 0, long = 0, radius = 0 }){
    const [r, currentLat, currentLng] = this.getOrbit(); 
    const newLat = clamp(currentLat + lat, -Math.PI/2, Math.PI/2);
    const newRadius = Math.max(0.1, r + radius);
    this.#position = latLngToCartesian([newRadius, newLat, currentLng - long]);
}
私たちは半径のための新しいコンポーネントをorbitBy . 私たちは新しい半径をクランプする必要があります.0では、位置が崩壊し、私たちはもはや私たちの回転は、以前のlat/longの複雑な追跡を行うので、私たちはただ、わずかに離れてセンターから離れている必要があると言うことができるかわからない.
また、倍数を行うことができます.私は数字パッドプラスマイナスキーでこれを行うに選択しています.インonKeydown イベントを設定します.
case "NumpadAdd": {
    this.cameras.default.zoomBy(2);
    break;
}
case "NumpadSubtract": {
    this.cameras.default.zoomBy(0.5);
    break;
}
zoomBy カメラで
zoomBy(value) {
    const [r, currentLat, currentLng] = this.getOrbit();
    const newRadius = Math.max(0.1, r / value);
    this.#position = latLngToCartesian([newRadius, currentLat, currentLng]);
}
ほとんど同じですが、我々は追加の代わりに乗算している.実際には“ズーム”は、我々は実際に逆を行っているし、2倍ズームの半径を半分にしていると0.5 xズームアウトする2倍になることを意味します.
そして今、我々はいくつかのより多くのオプションを本当にその照明の詳細を参照してください.
https://github.com/ndesmic/geogl/tree/v6