WebGL 3D 엔진 처음부터 3부분: 메쉬 변환

일단 기본적인 3d가 완성되면 나는 다음 단계에 어떻게 해야 할지 정말 확실하지 않다. 그러나 나는 코드를 구축하기 시작하는 곳이 있다고 생각한다. 왜냐하면 커다란 렌더링 기능에서 일이 좀 혼란스러워지기 때문이다.구체적으로 말하면 나는 어떻게 직관적인 방식으로 3d 대상의 격자를 생각해서 쉽게 사용할 수 있는지 생각하고 싶다.

격자류


솔직히 말해서, 나는 사물에 클래스를 설정하는 것을 그리 좋아하지 않는다. 왜냐하면 그것은 단지 하나의 대상 텍스트를 둘러싼 대량의 템플릿 파일이기 때문이다. 그러나 이런 상황에서 나는 이것이 의미가 있을 수 있다고 생각한다.이것은 분명히 말하지 않아도 알 수 있는 것이다. 유일하게 재미있는 것은 편의를 위해 나는 유형화된 수조 전환을 하고 있다. 나는 색인을'삼각형'이라고 부른다. 왜냐하면 그물의 각도에서 보면 이것이 바로 그것이기 때문이다.
export class Mesh {
    #positions;
    #colors;
    #normals;
    #uvs;
    #triangles;

    constructor(mesh){
        this.positions = mesh.positions;
        this.colors = mesh.colors;
        this.normals = mesh.normals;
        this.uvs = mesh.uvs;
        this.triangles = mesh.triangles;
    }

    set positions(val){
        this.#positions = new Float32Array(val);
    }
    get positions(){
        return this.#positions;
    }
    set colors(val) {
        this.#colors = new Float32Array(val);
    }
    get colors(){
        return this.#colors;
    }
    set normals(val) {
        this.#normals = new Float32Array(val);
    }
    get normals(){
        return this.#normals;
    }
    set uvs(val) {
        this.#uvs = new Float32Array(val);
    }
    get uvs(){
        return this.#uvs;
    }
    set triangles(val) {
        this.#triangles = new Uint16Array(val);
    }
    get triangles(){
        return this.#triangles;
    }
}

렌더러 업데이트


이제 렌더기에서 드로잉 함수를 분할합니다.새로운 connectedCallback:
async connectedCallback() {
    this.createShadowDom();
    this.cacheDom();
    this.attachEvents();
    await this.bootGpu();
    this.createMeshes();
    this.setupUniforms();
    this.render();
}
bootGpu 지금은 조금 줄었어요.
async bootGpu() {
    this.context = this.dom.canvas.getContext("webgl");
    this.program = this.context.createProgram();

    const vertexShader = compileShader(this.context, `
            uniform mat4 uProjectionMatrix;
            attribute vec3 aVertexPosition;
            attribute vec3 aVertexColor;
            float angle = -3.1415962 / 4.0;
            mat4 rotationY = mat4(
                cos(angle), 0, sin(angle), 0,
                0, 1, 0, 0,
                -sin(angle), 0, cos(angle), 0,
                0, 0, 0, 1
            );
            vec4 translateZ = vec4(0.0, 0.0, 2.0, 0.0);
            varying mediump vec4 vColor;
            void main(){
                gl_Position = uProjectionMatrix * (translateZ + rotationY * vec4(aVertexPosition, 1.0));
                vColor = vec4(aVertexColor, 1.0);
            }
        `, this.context.VERTEX_SHADER);
    const fragmentShader = compileShader(this.context, `
        varying lowp vec4 vColor;
        void main() {
            gl_FragColor = vColor;
        }
    `, this.context.FRAGMENT_SHADER);
    this.program = compileProgram(this.context, vertexShader, fragmentShader)
    this.context.enable(this.context.CULL_FACE);
    this.context.cullFace(this.context.BACK);
    this.context.useProgram(this.program);
}
이것은 색기 프로그램을 컴파일하고 (현재 우리는 하나뿐이지만, 잠시 후에 재구성할 수 있습니다.) 사용 중인 것으로 설정합니다.
다음으로는 createMeshes:
createMeshes(){
    this.meshes = {
        cube: new Mesh({
            positions: [
                //Front
                -0.5, -0.5, -0.5,
                0.5, -0.5, -0.5,
                0.5, 0.5, -0.5,
                -0.5, 0.5, -0.5,
                //Back
                0.5, -0.5, 0.5,
                -0.5, -0.5, 0.5,
                -0.5, 0.5, 0.5,
                0.5, 0.5, 0.5
            ],
            colors: [
                1.0, 0.0, 0.0,
                1.0, 0.0, 0.0,
                1.0, 0.0, 0.0,
                1.0, 0.0, 0.0,
                0.0, 1.0, 0.0,
                0.0, 1.0, 0.0,
                0.0, 1.0, 0.0,
                0.0, 1.0, 0.0
            ],
            triangles: [
                0, 1, 2, //front
                0, 2, 3,
                1, 4, 7, //right
                1, 7, 2,
                4, 5, 6, //back
                4, 6, 7,
                5, 0, 3, //left
                5, 3, 6,
                3, 2, 7, //top
                3, 7, 6,
                0, 4, 1, //bottom
                0, 5, 4
            ]
        })
    };
}
참고: 마지막부터 를 따르는 경우 밑면의 휘감기 순서가 잘못됩니다.위쪽이 맞습니다.
이것은 매우 좋다. 왜냐하면 현재 격자의 모든 데이터가 한 곳에 있기 때문이다.너는 쉽게 JSON이나 다른 물건에서 그것을 불러올 수 있다고 상상할 수 있다.우리도 대상에 다른 격자를 추가한 후에 그 중에서 순환할 수 있다.setupUniforms는 동일합니다.투영 행렬을 설정하고 있습니다.
setupUniforms(){
        const projectionMatrix = new Float32Array(getProjectionMatrix(this.#height, this.#width, 90, 0.01, 100).flat());
        const projectionLocation = this.context.getUniformLocation(this.program, "uProjectionMatrix");
        this.context.uniformMatrix4fv(projectionLocation, false, projectionMatrix);
    }
render 업데이트:
render() {
    this.context.clear(this.context.COLOR_BUFFER_BIT | this.context.DEPTH_BUFFER_BIT);
    for (const mesh of Object.values(this.meshes)){
        this.bindMesh(mesh);
        this.context.drawElements(this.context.TRIANGLES, mesh.triangles.length, this.context.UNSIGNED_SHORT, 0);
    }
}
우리는 먼저 격자를 제거한 다음에 격자를 교체하고bind를 호출한 다음 그것을 그립니다.bindMesh 이것은 이전의 모든 귀속 버퍼 구역의 설정 호출이 발생한 곳이다.
bindMesh(mesh){
    this.bindPositions(mesh.positions);
    this.bindColors(mesh.colors);
    this.bindIndices(mesh.triangles);;
}
코드의 나머지 부분은 기본적으로 같기 때문에, 유일한 차이점은 우리가 격자에서 값을 얻는 것이지, 하드코딩을 하는 것이 아니다.나는 그것들bind을 다시 명명했다. 이것이 실제 상황이기 때문에 우리는 격자 값을 GPU의 버퍼에 연결하고 그것들을 속성과 연결시켰다.
모든 것은 예전처럼 일해야 하지만, 더욱 쉽게 처리해야 한다.입방체를 사각뿔체로 교체해 봅시다.
export const quadPyramid = {
    positions: [
        0.0, 0.5, 0.0,
        -0.5, -0.5, -0.5,
        0.5, -0.5, -0.5,
        0.5, -0.5, 0.5,
        -0.5, -0.5, 0.5 
    ],
    colors: [
        1.0, 0, 0,
        0, 0, 1,
        0, 0, 1,
        0, 0, 1,
        0, 0, 1
    ],
    triangles: [
        0, 1, 2,
        0, 2, 3,
        0, 3, 4,
        0, 4, 1
    ]
}

격자 레벨 변환


우리도 격자 자체를 변환할 수 있다.GPU가 이 일을 더 빨리 완성할 수 있기 때문에 좋은 생각은 아니지만, 만약 당신이 생각한다면, 우리는 이 점을 추가할 수 있다. 이것은 적어도 일이 정확하다는 것을 증명하는 데 쓰일 수 있다.
메서드에 메서드translate를 추가할 수 있습니다.
translate(x = 0, y = 0, z = 0){
    for(let i = 0; i < this.#positions.length; i++){
        switch(i % 3){
            case 0:
                this.#positions[i] += x;
                break;
            case 1:
                this.#positions[i] += y;
                break;
            case 2:
                this.#positions[i] += z;
                break;
        }
    }
}
이것은 좌표를 업데이트해서 이동합니다. (마찬가지로 실천에서는 기선 좌표를 상대적으로 가운데에 두고 행렬 곱셈을 사용해서 이동하기를 원합니다.)
동일한 캔버스에 두 개의 메쉬를 배치하여 몇 가지 변경 사항만 적용할 수 있습니다.
createMeshes(){
    const tcube = new Mesh(cube);
    tcube.translate(0.75, 0, 0);
    const tpyramid = new Mesh(quadPyramid);
    tpyramid.translate(-0.75, 0, 0);
    this.meshes = {
        pyramid: tpyramid,
        cube: tcube
    };
}

이제 우리는 여러 개의 격자를 쉽게 그릴 수 있다.

초점이동, 줌 및 회전


우리로 하여금 이렇게 하게 하는 것은 진정한 방식이다.우리가 원하는 것은 격자에 있는 값을 저장하고 격자를 그릴 때, 이 필드를 표시하는 행렬을 전송해서 GPU가 정점 착색기에서 작업을 완성하고, 밑바닥 격자를 수정할 필요가 없도록 할 수 있다.초점이동, 줌 및 회전을 나타내는 메쉬에 속성을 추가합니다.
#translation = new Float32Array([0, 0, 0]);
#scale = new Float32Array([1, 1, 1]);
#rotation = new Float32Array([0, 0, 0]);
setTranslation({ x, y, z }){
    if (x){
        this.#translation[0] = x;
    }
    if (y) {
        this.#translation[1] = y;
    }
    if (z) {
        this.#translation[2] = z;
    }
}
getTranslation(){
    return this.#translation;
}
setScale({ x, y, z }) {
    if (x) {
        this.#scale[0] = x;
    }
    if (y) {
        this.#scale[1] = y;
    }
    if (z) {
        this.#scale[2] = z;
    }
}
getScale() {
    return this.#scale;
}
setRotation({ x, y, z }) {
    if (x) {
        this.#rotation[0] = x;
    }
    if (y) {
        this.#rotation[1] = y;
    }
    if (z) {
        this.#rotation[2] = z;
    }
}
getRotation(){
    return this.#rotation;
}
나는 네가 갱신하고 싶은 부품을 통해 인체공학에 부합하도록 해 보았다.기본 데이터 유형은 aFloat32Array입니다. WebGL이 바인딩할 때 기대하는 것이기 때문입니다.
그런 다음 각 메쉬의 값을 결합하는 방법bindUniforms을 만들 수 있습니다.
bindMesh(mesh){
    this.bindPositions(mesh.positions);
    this.bindColors(mesh.colors);
    this.bindIndices(mesh.triangles);
    this.bindUniforms(mesh.getTranslation(), mesh.getScale(), mesh.getRotation());
}
bindUniforms(translation, scale, rotation){
    const translationLocation = this.context.getUniformLocation(this.program, "uTranslation");
    this.context.uniform3fv(translationLocation, translation);
    const scaleLocation = this.context.getUniformLocation(this.program, "uScale");
    this.context.uniform3fv(scaleLocation, scale);
    const rotationLocation = this.context.getUniformLocation(this.program, "uRotation");
    this.context.uniform3fv(rotationLocation, rotation);
}
교점 셰이더에서 지원을 추가할 수 있습니다.
uniform mat4 uProjectionMatrix;
uniform vec3 uTranslation;
uniform vec3 uScale;
uniform vec3 uRotation;

attribute vec3 aVertexPosition;
attribute vec3 aVertexColor;
varying mediump vec4 vColor;
void main(){
    mat4 translation = mat4(
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        uTranslation[0], uTranslation[1], uTranslation[2], 1
    );
    mat4 scale = mat4(
        uScale[0], 0, 0, 0,
        0, uScale[1], 0, 0,
        0, 0, uScale[2], 0,
        0, 0, 0, 1
    );
    mat4 rotationX = mat4(
        1, 0, 0, 0,
        0, cos(uRotation[0]), -sin(uRotation[0]), 0,
        0, sin(uRotation[0]), cos(uRotation[0]), 0,
        0, 0, 0, 1
    );
    mat4 rotationY = mat4(
        cos(uRotation[1]), 0, sin(uRotation[1]), 0,
        0, 1, 0, 0,
        -sin(uRotation[1]), 0, cos(uRotation[1]), 0,
        0, 0, 0, 1
    );
    mat4 rotationZ = mat4(
        cos(uRotation[2]), -sin(uRotation[2]), 0, 0,
        sin(uRotation[2]), cos(uRotation[2]), 0, 0,
        0, 0, 1, 0,
        0, 0, 0, 1
    );
    mat4 modelMatrix = translation * scale * rotationX * rotationY * rotationZ;
    gl_Position = uProjectionMatrix * modelMatrix * vec4(aVertexPosition, 1.0);
    vColor = vec4(aVertexColor, 1.0);
}
변환, 회전 및 배율 조정 행렬을 생성합니다.사실, 나는glsl이 열의 주 정렬을 사용했기 때문에 완전히 망쳤다.아이고, 이 점을 주의하십시오. (나는 왜 회전 행렬이 작동할 수 있는지 완전히 이해하지 못하지만, 정의가 정확하지 않으면 전환되지 않습니다.)참고로 평이, 회전, 축소에 쓰이는 이 행렬들은 교과서의 표준이다.나는 그들이 어떻게 일을 하는지 토론하지 않을 것이다. 왜냐하면 많은 자원이 그들을 공정하게 대할 수 있기 때문이다. 만약 당신이 그것을 필요로 한다면, 당신은 그것들을 찾을 수 있기 때문이다.
createMeshes(){
    const tcube = new Mesh(cube);
    tcube.setRotation({ x: Math.PI / 4,  y: Math.PI / 4 });
    tcube.setTranslation({ z: 2, x: 0.75 });
    const tpyramid = new Mesh(quadPyramid);
    tpyramid.setTranslation({ z: 2, x: -0.75 });
    this.meshes = {
        pyramid: tpyramid,
        cube: tcube
    };
}
우리는 지금 가볍게 마음대로 회전하고 평이할 수 있다.그러나 기억해 주십시오. 우리는 모양을 약간 뒤로 밀어야 합니다. 이렇게 하면 카메라가 그것들 안에 들어가지 않기 때문에translate-z가 없으면 당신은 아무것도 볼 수 없습니다!

모델 매트릭스 생성하기


이것은 좋지만 효과가 그다지 좋지 않다.우리가 하고 있는 모든 행렬 연산을 고려해 보자.만약 이 격자에 10000개의 정점이 있다면, 우리는 10000번을 할 것이다!이 경우 셰이더를 실행하기 전에 CPU에서 예상 계산을 하는 것이 좋습니다.
mat4 modelMatrix = translation * scale * rotationX * rotationY * rotationZ;
이 말은 매우 중요하다.나는 설명하지 않았지만, 우리는 기본적으로 모든 행렬을 미리 계산된 행렬로 곱할 수 있다.이 점도 매우 중요하다. 왜냐하면 순서가 매우 중요하기 때문이다.변환한 다음 회전하면 회전 후 변환과 다릅니다(이렇게 이상하게 보이면 입방체를 45도 회전한 다음 오른쪽으로 2개의 단위를 변환한 다음 원점을 중심으로 45도 회전하는 것을 고려할 수 있습니다).사실 우리는 여러 작업을 다른 순서로 반복할 수 있기 때문에 복구하는 것은 무의미하다. (비록 간단하게 보기 위해서 복구할 것이지만 GPU에서 데이터 구조로 옮기는 것은 미래의 개선을 더욱 쉽게 할 것이다.)
정의된 행렬 연산을 시작합니다.
//vector.js
export function getRotationXMatrix(theta) {
    return [
        [1, 0, 0, 0],
        [0, Math.cos(theta), -Math.sin(theta), 0],
        [0, Math.sin(theta), Math.cos(theta), 0],
        [0, 0, 0, 1]
    ];
}

export function getRotationYMatrix(theta) {
    return [
        [Math.cos(theta), 0, Math.sin(theta), 0],
        [0, 1, 0, 0],
        [-Math.sin(theta), 0, Math.cos(theta), 0],
        [0, 0, 0, 1]
    ];
}

export function getRotationZMatrix(theta) {
    return [
        [Math.cos(theta), -Math.sin(theta), 0, 0],
        [Math.sin(theta), Math.cos(theta), 0, 0],
        [0, 0, 1, 0],
        [0, 0, 0, 1]
    ];
}

export function getTranslationMatrix(x, y, z) {
    return [
        [1, 0, 0, x],
        [0, 1, 0, y],
        [0, 0, 1, z],
        [0, 0, 0, 1]
    ];
}

export function getScaleMatrix(x, y, z){
    return [
        [x, 0, 0, 0],
        [0, y, 0, 0],
        [0, 0, z, 0],
        [0, 0, 0, 1]
    ];
}

export function multiplyMatrix(a, b) {
    const matrix = [
        new Array(4),
        new Array(4),
        new Array(4),
        new Array(4)
    ];
    for (let c = 0; c < 4; c++) {
        for (let r = 0; r < 4; r++) {
            matrix[r][c] = a[r][0] * b[0][c] + a[r][1] * b[1][c] + a[r][2] * b[2][c] + a[r][3] * b[3][c];
        }
    }

    return matrix;
}

export function getIdentityMatrix() {
    return [
        [1, 0, 0, 0],
        [0, 1, 0, 0],
        [0, 0, 1, 0],
        [0, 0, 0, 1]
    ];
}
export function transpose(matrix){
    return [
        [matrix[0][0], matrix[1][0], matrix[2][0], matrix[3][0]],
        [matrix[0][1], matrix[1][1], matrix[2][1], matrix[3][1]],
        [matrix[0][2], matrix[1][2], matrix[2][2], matrix[3][2]],
        [matrix[0][3], matrix[1][3], matrix[2][3], matrix[3][3]]
    ];
}
코드는 많지만 단도직입적일 것이다.mesh.js 파일에서 #rotation, #scale#translation를 일반 배열로 변경하고 새 메서드를 추가합니다.
getModelMatrix(){
    return new Float32Array(transpose(multiplyMatrix(
        getRotationXMatrix(this.#translation[0]),
            multiplyMatrix(
                getRotationYMatrix(this.#rotation[1]),
                multiplyMatrix(
                    getRotationZMatrix(this.#rotation[2]),
                    multiplyMatrix(
                        getScaleMatrix(this.#scale[0], this.#scale[1], this.#scale[2]),
                        multiplyMatrix(
                            getTranslationMatrix(this.#translation[0], this.#translation[1], this.#translation[2]),
                            getIdentityMatrix()
                        )
                    )
                )
            )
        )).flat());
}
정말 징그럽지만, 우리는 반드시 이렇게 해야 한다.상술한 주요 원인에서 당신도 그것을 옮겨야 합니다.그것은 더 복잡한 행렬로 정리할 수 있을지도 모르지만, 나는 지금 말하지 않겠다.제복을 갱신하라. 그러면 우리는 하나밖에 없다.
bindMesh(mesh){
    this.bindPositions(mesh.positions);
    this.bindColors(mesh.colors);
    this.bindIndices(mesh.triangles);
    this.bindUniforms(mesh.getModelMatrix());
}
bindUniforms(modelMatrix){
    const modelMatrixLocation = this.context.getUniformLocation(this.program, "uModelMatrix");
    this.context.uniformMatrix4fv(modelMatrixLocation, false, modelMatrix);
}
그 교활한 전환 파라미터를 조심해라, 그것은 조용히 들어올 것이다.행렬에는 세 개의 매개 변수가 있다.교점 셰이더는 간단할 수도 있습니다.
uniform mat4 uProjectionMatrix;
uniform mat4 uModelMatrix;

attribute vec3 aVertexPosition;
attribute vec3 aVertexColor;
varying mediump vec4 vColor;
void main(){
    gl_Position = uProjectionMatrix * modelMatrix * vec4(aVertexPosition, 1.0);
    vColor = vec4(aVertexColor, 1.0);
}
좋아, 간단한 전환과 효율!
우리는 지금 정말로 코드펜의 제한에 대항하고 있다.아마도 다음에 우리는git 환매를 진행할 충분한 자금이 있을 것이다.

좋은 웹페이지 즐겨찾기