imuncle / imuncle.github.io

大叔的个人小站
https://imuncle.github.io/
78 stars 17 forks source link

基于canvas生成3D立体画 #104

Open imuncle opened 4 years ago

imuncle commented 4 years ago

3D立体画是裸眼3D的一种,最初是由一些极具创意的画家在街道或平地上创作的,后来又有了在纸上的绘制的作品。我的第一幅3D立体画是高二上画的:

image

然后上大学后许久没有画过了。

近期武汉新型冠状病毒肺炎疫情严重,只能呆在家,闲来无事想写一个能直接生成3D立体画的程序。

思路很简单,我们的目的是在相机的图像中将需要显示的物体放置在纸面上,还是放出这个世界坐标和图像坐标的关系:

image

首先定义一个相机:

// 模拟相机类
var Camera = function() {
    var arr = [[1000, 0, 400],
               [0, 1000, 300],
               [0, 0, 1]];
    this.intrinsic = new Matrix(arr);   // 相机内参
    var arr1 =  [[1, 0, 0, 0],
                [0, 1, 0, 0],
                [0, 0, 1, 0]];
    this.extrinsic = new Matrix(arr1);  //相机外参
    var arr2 = [[1, 0, 0],
                [0, 1, 0],
                [0, 0, 1]];
    this.rotate = new Matrix(arr2);     // 相机旋转矩阵
    this.camera_angle = 0;
    this.camera_distance = 100;
    this.camera_height = 100;
    this.camera_move = 0;
}

然后我还创建了一个矩阵相乘类:

// 简单的矩阵库,只支持矩阵乘法
var Matrix = function(matrix) {
    this.matrix = matrix;
    this.rows = this.matrix.length;     // 行数y
    this.cols = this.matrix[0].length;  // 列数x
}

// 矩阵相乘
Matrix.prototype.dot = function(matrix) {
    var self_matrix = this;
    var cols = matrix.cols;
    var rows = this.rows;
    var result = new Array(rows);
    for(var i = 0; i < rows; i++) {
        result[i] = new Array(cols);
    }
    for(var i = 0; i < cols; i++) {
        for(var j = 0; j < rows; j++) {
            var sum = 0;
            for(var k = 0; k < self_matrix.cols; k++) {
                sum += self_matrix.matrix[j][k] * matrix.matrix[k][i];
            }
            result[j][i] = sum;
        }
    }
    var new_matrix = new Matrix(result);
    return new_matrix;
}

最关键的是接下来这个paper类,我模拟了一个A4纸,A4四个角点的世界坐标是已知给定的,根据相机内参和外参计算出其在图像中的坐标,就可以显示A4纸了。

// 纸张以中心旋转
Paper.prototype.rotate = function() {
    var arr =  [[Math.cos(this.paper_angle), -Math.sin(this.paper_angle)],
                [Math.sin(this.paper_angle), Math.cos(this.paper_angle)]];
    var rotate = new Matrix(arr);
    var corners = [
        [[-105], [-148.5]],
        [[105], [-148.5]],
        [[105], [148.5]],
        [[-105], [148.5]]
    ];
    for(var i = 0; i < corners.length; i++) {
        var point = new Matrix(corners[i]);
        var result = rotate.dot(point);
        this.corners[i][0] = result.matrix[0][0] + 105;
        this.corners[i][1] = result.matrix[1][0] + 148.5;
    }
}

// 在canvas上显示纸张
Paper.prototype.show = function(context1) {
    this.rotate();
    var arr =  [[1, 0, 0, -this.height/2 + self.camera.camera_move],
                [0, 1, 0, self.camera.camera_height],
                [0, 0, 1, self.camera.camera_distance]];
    self.camera.extrinsic.set(arr);
    var arr1 = [[1, 0, 0],
                [0, Math.cos(self.camera.camera_angle), -Math.sin(self.camera.camera_angle)],
                [0, Math.sin(self.camera.camera_angle), Math.cos(self.camera.camera_angle)]];
    self.camera.rotate.set(arr1);
    self.camera.extrinsic = self.camera.rotate.dot(self.camera.extrinsic);
    context1.beginPath();
    var point = new Matrix([[this.corners[3][0]], [0], [this.corners[3][1]], [1]]);
    var result = self.camera.intrinsic.dot(self.camera.extrinsic).dot(point);
    context1.moveTo(result.matrix[0][0] / result.matrix[2][0], result.matrix[1][0] / result.matrix[2][0]);
    // 从左下角逆时针
    for(var i = 0; i < this.corners.length; i++) {
        point = new Matrix([[this.corners[i][0]], [0], [this.corners[i][1]], [1]]);
        result = self.camera.intrinsic.dot(self.camera.extrinsic).dot(point);
        context1.lineTo(result.matrix[0][0] / result.matrix[2][0], result.matrix[1][0] / result.matrix[2][0]);
    }
    context1.strokeStyle = 'red';
    context1.lineWidth = 1;
    context1.stroke();
}

最后是生成图像的代码,逻辑很简单,遍历A4中的每个像素点,投影到图像坐标系,获取到对应的像素值。

Generate.prototype.show = function(context1, context2) {
    var context1 = self.canvas1.getContext('2d');
    var context2 = self.canvas2.getContext('2d');
    context2.clearRect(0, 0, self.canvas2.width, self.canvas2.height);
    var imageData = context2.getImageData(0, 0, self.canvas2.width, self.canvas2.height);
    var img_data = imageData.data;
    for(var i = 0; i < self.paper.width; i++) {
        for(var j = 0; j < self.paper.height; j++) {
            var point = [j, i];
            point = self.paper.rotate_point(point);
            var temp_point = new Matrix([[point[0]], [0], [point[1]], [1]]);
            var result = self.camera.intrinsic.dot(self.camera.extrinsic).dot(temp_point);
            var pixel = context1.getImageData(result.matrix[0][0] / result.matrix[2][0], result.matrix[1][0] / result.matrix[2][0], 1, 1);
            var data = pixel.data;
            img_data[4 * (i + j * self.paper.width)] = data[0];
            img_data[4 * (i + j * self.paper.width) + 1] = data[1];
            img_data[4 * (i + j * self.paper.width) + 2] = data[2];
            img_data[4 * (i + j * self.paper.width) + 3] = data[3];
        }
    }
    context2.putImageData(imageData, 0, 0);
}

最后,我把代码上传到了github上:https://github.com/imuncle/3Ddraw

在线体验:https://imuncle.github.io/3Ddraw/

XieSheng-qmaker commented 4 years ago

强啊!以后就不需要手画了,直接打印出来

FENYUN323 commented 4 years ago

老板牛批