DIYPlay
블로그

3D 렌더링 DIY 프로젝트 1 - 정점 변환

DIYPlayer

·

2달 전

3d
렌더링
변환행렬
정점변환

3D 렌더링을 구현하기 위한 준비 단계로 물체를 표현하는 정점들을 트랜스폼 행렬변환을 통해 3D 세계에 배치해보는 것을 목표로 합니다.

0. 캔버스 준비하기

DIY 에디터에서 테스트 해볼 수 있게 html 페이지에 canvas를 추가하고 초기화 합니다.

const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 800;
canvas.style.backgroundColor = '#000000';
document.body.append(canvas);
const context = canvas.getContext('2d');

 

윈도우 크기에 맞게 canvas를 확대&축소 하는 코드를 추가합니다.

function onResize() {
    const width = window.innerWidth;
    const height = window.innerHeight;
    if(width > height) {
        canvas.style.width = `${height}px`;
        canvas.style.height = `${height}px`;
    }
    else {
        canvas.style.width = `${width}px`;
        canvas.style.height = `${width}px`;
    }
}
window.addEventListener('resize', onResize);
onResize();

 

1. 변환 행렬 만들기

열 벡터를 기준으로 하는 변환 행렬을 만듭니다.

 class Matrix4x4 {
    constructor() {
       this.matrix = [
            [1, 0, 0, 0],
            [0, 1, 0, 0],
            [0, 0, 1, 0],
            [0, 0, 0, 1],
        ]
    }

    applyToVertex(vertex) {
        let x = 0, y = 0, z = 0, w = 0;

        x = this.matrix[0][0] * vertex.x +
            this.matrix[0][1] * vertex.y + 
            this.matrix[0][2] * (vertex.z || 0) +
            this.matrix[0][3] * 1;

        y = this.matrix[1][0] * vertex.x +
            this.matrix[1][1] * vertex.y + 
            this.matrix[1][2] * (vertex.z || 0) +
            this.matrix[1][3] * 1;

        z = this.matrix[2][0] * vertex.x + 
            this.matrix[2][1] * vertex.y +  
            this.matrix[2][2] * (vertex.z || 0) + 
            this.matrix[2][3] * 1;

        return {x, y, z};
    }

    multiply(other) {
        const result = [
            [1, 0, 0, 0],
            [0, 1, 0, 0],
            [0, 0, 1, 0],
            [0, 0, 0, 1],
        ];
        //result.matrix[0][0] = 
        //     this.matrix[0][0] * other.matrix[0][0] + 
        //     this.matrix[0][1] * other.matrix[1][0] + 
        //     this.matrix[0][2] * other.matrix[2][0] + 
        //     this.matrix[0][3] * other.matrix[3][0];
        // result.matrix[0][1] = 
        //     this.matrix[0][0] * other.matrix[0][1] + 
        //     this.matrix[0][1] * other.matrix[1][1] + 
        //     this.matrix[0][2] * other.matrix[2][1] + 
        //     this.matrix[0][3] * other.matrix[3][1];

        for(let row = 0; row < 4; row++) {
            for(let col = 0; col < 4; col++) {
                let sum = 0;
                for(let i = 0; i < 4; i++) {
                    sum += this.matrix[row][i] * other.matrix[i][col];
                }
                result[row][col] = sum;
                // result.matrix[row][col] = 
                //     this.matrix[row][0] * other.matrix[0][col] + 
                //     this.matrix[row][1] * other.matrix[1][col] + 
                //     this.matrix[row][2] * other.matrix[2][col] + 
                //     this.matrix[row][3] * other.matrix[3][col];
            }
        }
        this.matrix = result;
        return this;
    }


    static translate(x, y, z) {
        const matrix = new Matrix4x4();
        matrix.matrix = [
            [1, 0, 0, x],
            [0, 1, 0, y],
            [0, 0, 1, z],
            [0, 0, 0, 1],
        ];
        return matrix;
    }

    static scale(x, y, z) {
        const matrix = new Matrix4x4();
        matrix.matrix = [
            [x, 0, 0, 0],
            [0, y, 0, 0],
            [0, 0, z, 0],
            [0, 0, 0, 1],
        ];
        return matrix;
    }

    static rotateX(angle) {
        const rad = (Math.PI/180) * angle;
        const cos = Math.cos(rad);
        const sin = Math.sin(rad);

        const matrix = new Matrix4x4();
        matrix.matrix = [
            [    1,    0,    0,    0],
            [    0,  cos, -sin,    0],
            [    0,  sin,  cos,    0],
            [    0,    0,    0,    1],
        ];
        return matrix;
    }

    static rotateY(angle) {
        const rad = (Math.PI/180) * angle;
        const cos = Math.cos(rad);
        const sin = Math.sin(rad);

        const matrix = new Matrix4x4();
        matrix.matrix = [
            [  cos,    0,  sin,    0],
            [    0,    1,    0,    0],
            [ -sin,    0,  cos,    0],
            [    0,    0,    0,    1],
        ];
        return matrix;
    }

    static rotateZ(angle) {
        const rad = (Math.PI/180) * angle;
        const cos = Math.cos(rad);
        const sin = Math.sin(rad);

        const matrix = new Matrix4x4();
        matrix.matrix = [
            [  cos, -sin,    0,    0],
            [  sin,  cos,    0,    0],
            [    0,    0,    1,    0],
            [    0,    0,    0,    1],
        ];
        return matrix;
    }
}

 

2. 2D 좌표 변환해보기

만들어진 변환 행렬의 동작도 테스트 해볼 겸 2D 좌표의 정점들을 변환해보는 작업을 해보겠습니다.

const rectPoints = [
    { position: {x: -50, y: -50}, color: '#ff0000' },
    { position: {x:  50, y: -50}, color: '#00ff00' },
    { position: {x:  50, y:  50}, color: '#0000ff' },
    { position: {x: -50, y:  50}, color: '#ffff00' },
];

사각형의 네 꼭지점을 좌표로 만들고 각각의 꼭지점을 구분할 수 있도록 색을 부여하겠습니다.

 

각 꼭지점은 작은 사각형을 그려 표현하겠습니다.

function drawPoint(pos, color) {
    const w = 10;
    const h = 10;
    context.fillStyle = color;
    context.fillRect(pos.x - w/2, pos.y - h/2, w, h);
}

 

변환 행렬과 꼭지점 데이터를 입력하면 각 꼭지점 좌표에 변환 행렬을 적용하고 그려주는 함수를 만듭니다.

한번에 여러 개의 변환된 사각형을 그렸을 때 알아볼 수 있게 라벨도 같이 그려줍니다. 

function transformAndDraw(pointDatas, mat, label) {
    for(let i = 0; i < pointDatas.length; i++) {
        const pointData = pointDatas[i];
        let position = pointData.position;
        if(mat) {
            position = mat.applyToVertex(position);
        }
        const color = pointData.color;
        drawPoint(position, color);
    }

    if(label) {
        let center = {x:0, y:0};
        if(mat) {
            center = mat.applyToVertex(center);
        }
        context.strokeStyle = '#ffffff';
        context.strokeText(label, center.x, center.y);
    }
}

 

변환 행렬을 적용해 사각형을 그려보겠습니다.

transformAndDraw(rectPoints, undefined, '0');

mat = Matrix4x4.translate(200, 100, 0);
transformAndDraw(rectPoints, mat, '1');

mat = new Matrix4x4();
mat = mat.multiply(Matrix4x4.translate(400, 100, 0));
mat = mat.multiply(Matrix4x4.rotateZ(45));
mat = mat.multiply(Matrix4x4.scale(1,1,1));
transformAndDraw(rectPoints, mat, '2');

mat = new Matrix4x4();
mat = mat.multiply(Matrix4x4.translate(650, 150, 0));
mat = mat.multiply(Matrix4x4.rotateZ(45));
mat = mat.multiply(Matrix4x4.scale(2,2,1));
transformAndDraw(rectPoints, mat, '3');

mat = new Matrix4x4();
mat = mat.multiply(Matrix4x4.rotateZ(45));
mat = mat.multiply(Matrix4x4.translate(650, 150, 0));
mat = mat.multiply(Matrix4x4.scale(2,2,1));
transformAndDraw(rectPoints, mat, '4');


 

3. 3D 좌표 변환해보기

다음 단계로 3D 좌표를 그려보겠습니다.

z축 좌표 하나가 추가되었을 뿐 전 단계와 크게 다르지 않습니다.

 

정사각형 물체를 구성하는 정점들을 준비합니다.

const pointData = [
    { position: {x: -50, y: -50,z: -50}, color: '#ff0000' },
    { position: {x:  50, y: -50,z: -50}, color: '#00ff00' },
    { position: {x:  50, y:  50,z: -50}, color: '#0000ff' },
    { position: {x: -50, y:  50,z: -50}, color: '#ffff00' },

    { position: {x: -50, y: -50,z:  50}, color: '#ff00ff' },
    { position: {x:  50, y: -50,z:  50}, color: '#00ffff' },
    { position: {x:  50, y:  50,z:  50}, color: '#FF9390' },
    { position: {x: -50, y:  50,z:  50}, color: '#FF9200' },
];

 

꼭지점 자리에 정점 인덱스를 표시해 주도록 변경하겠습니다.

function drawPoint(pos, color, label) {
    context.strokeStyle = color;
    context.font = '20pt aria';
    context.strokeText(label, pos.x, pos.y);
}

function transformAndDraw(pointDatas, mat) {
    for(let i = 0; i < pointDatas.length; i++) {
        const pointData = pointDatas[i];
        let position = pointData.position;
        if(mat) {
            position = mat.applyToVertex(position);
        }
        const color = pointData.color;
        drawPoint(position, color, i.toString());
    }
}

 

변환 행렬을 적용해 그려줍니다.

let mat = new Matrix4x4();
mat = mat.multiply(Matrix4x4.translate(400, 400, 0));
mat = mat.multiply(Matrix4x4.rotateX(15));
mat = mat.multiply(Matrix4x4.rotateY(30));
mat = mat.multiply(Matrix4x4.scale(3,3,3));
transformAndDraw(pointData, mat);

4. 정육면체 그리기

꼭지점 만으로는 3D 변환을 확인이 어려워 꼭지점을 선으로 이어서 면을 표현해보겠습니다.

 

앞에서 만들었던 8개의 꼭지점 좌표를 이용해 면을 만들어 줍니다.

사각형으로 6개의 면을 그려도 되지만 3D 렌더링에서 대부분 많이 사용되는 방식인 삼각형으로 면을 구성해보겠습니다.

어떤 정점들로 구성되는지 정점의 인덱스 값으로 면을 정의합니다.

const faceData = [
    [0, 1, 2],   //앞면
    [0, 2, 3],   //앞면
    
    [4, 5, 1],   //윗면
    [4, 1, 0],   //윗면

    [1, 5, 6],   //오른면
    [1, 6, 2],   //오른면
    
    [4, 0, 3],   //왼면
    [4, 3, 7],   //왼면

    [3, 2, 6],   //아랫면
    [3, 6, 7],   //아랫면

    [5, 4, 7],   //뒷면
    [5, 7, 6],   //뒷면
];

 

세 점을 입력하면 해당 좌표로 삼각형을 그려주는 함수를 만듭니다.

function drawFace(p1, p2, p3, color) {
    context.strokeStyle = color;
    context.beginPath();
    context.moveTo(p1.x, p1.y);
    context.lineTo(p2.x, p2.y);
    context.lineTo(p3.x, p3.y);
    context.lineTo(p1.x, p1.y);
    context.stroke();
}
function transformAndDraw(mat) {
    for(let i = 0; i < faceData.length; i++) {
        const f1 = faceData[i][0];
        const f2 = faceData[i][1];
        const f3 = faceData[i][2];
        const p1 = mat.applyToVertex(vertexData[f1].position);
        const p2 = mat.applyToVertex(vertexData[f2].position);
        const p3 = mat.applyToVertex(vertexData[f3].position);
        
        
drawFace(p1, p2, p3, '#ffffffbb');
    }

    for(let i = 0; i < vertexData.length; i++) {
        const pointData = vertexData[i];
        const position = mat.applyToVertex(pointData.position);
        const color = pointData.color;
        drawPoint(position, color, i.toString());
    }
}

 

변환 행렬을 적용하고 그려보겠습니다.

let mat = new Matrix4x4();
mat = mat.multiply(Matrix4x4.translate(400, 400, 0));
mat = mat.multiply(Matrix4x4.rotateX(15));
mat = mat.multiply(Matrix4x4.rotateY(30));
mat = mat.multiply(Matrix4x4.scale(3,3,3));
transformAndDraw(mat);

 

보이는 면과 보이지 않는 면의 구분이 안되어 복잡하므로 가려져서 보이지 않는 면은 흐리게 그려 주는 백페이스 컬링 코드를 추가합니다.

...
const ab = Vector3.sub(p2, p1);
const ac = Vector3.sub(p3, p1);
const c = Vector3.cross(ab, ac);
const d = Vector3.dot(c, {x:0, y: 0, z: 1});
        
if(d > 0) {
    drawFace(p1, p2, p3, '#ffffffbb');
}
else {
    drawFace(p1, p2, p3, '#ffffff20');
}
...

 

정육면체를 실시간으로 회전 시키는 코드를 작성해보고 마무리 하겠습니다.

const transform = {
    position: {x: 400, y: 400, z: 0},
    rotation: {x: 15, y: 0, z: 0},
    scale: {x: 3, y: 3, z: 3},
}

function animate() {
    context.clearRect(0, 0, canvas.width, canvas.height);

    transform.rotation.y += 1;
    transform.rotation.z += 1;

    let mat = new Matrix4x4();
    mat = mat.multiply(Matrix4x4.translate(
        transform.position.x, transform.position.y, transform.position.z));
    mat = mat.multiply(Matrix4x4.rotateX(transform.rotation.x));
    mat = mat.multiply(Matrix4x4.rotateY(transform.rotation.y));
    mat = mat.multiply(Matrix4x4.rotateZ(transform.rotation.z));
    mat = mat.multiply(Matrix4x4.scale(
        transform.scale.x, transform.scale.y, transform.scale.z
    ));
    transformAndDraw(mat);

    requestAnimationFrame(animate);
}

animate();

 

 

프로젝트 바로 가기

3D 렌더링 DIY 1 - 정점변환

이전 글

3D 렌더링 DIY 프로젝트 2 - 래스터라이즈

다음 글

3D 렌더링 DIY 프로젝트 - 소개

댓글 0
    서비스 이용약관|개인정보 보호정책

    Copyright © DIYPlay All rights reserved.