NB的程序员,亮瞎了你的眼吗?

郑重声明: 本文首发于人工博客html

一、导读

你能想象到1K的代码能写出什么样的功能强大、效果炫酷的做品吗?来吧,今天小编带领你们认识下下面这位大神的做品。程序员

西班牙程序员Roman Cortes用纯JavaScript脚本编写的玫瑰花。
这才是牛逼程序员送给女朋友的最好情人节礼物呢!(提示:在不一样浏览器下观看效果、速度会有很大的不一样)算法

二、先来张效果图

rose

在线预览
预览效果1:canvas

预览效果2数组

三、原理解读

3.1 蒙特卡罗方法

蒙特卡罗方法是使人难以置信的强大的工具。我用他们全部的时间,对于不少类型的函数优化和抽样问题,他们几乎像魔法同样当你有更多的CPU时间比设计和编码算法。在上升的状况下,它是很是有用的代码大小的优化。
若是你不知道不少关于蒙特卡罗方法,你能够读到他们在这个优秀的维基百科文章。浏览器

3.2 明确的表面和采样/绘图

定义的形状玫瑰我使用多个explicit-defined表面。我使用一个共有31表面:24花瓣,萼片4(周围的薄叶花瓣),2叶和1的玫瑰。
这些显式的表面如此,它们是如何工做的?它是很容易的,我要提供一个二维的例子:
首先我定义明确的表面功能:app

function surface(a, b) {  // I'm using a and b as parameters ranging from 0 to 1.
    return {
        x: a*50,
        y: b*50
    };
    // this surface will be a square of 50x50 units of size
}

而后,画它的代码:dom

var canvas = document.body.appendChild(document.createElement("canvas")),
    context = canvas.getContext("2d"),
    a, b, position;

// Now I'm going to sample the surface at .1 intervals for a and b parameters:

for (a = 0; a < 1; a += .1) {
    for (b = 0; b < 1; b += .1) {
        position = surface(a, b);
        context.fillRect(position.x, position.y, 1, 1);
    }
}

结果:ide

人工博客

如今,让咱们尝试更多密集采样间隔(比间隔=更稠密采样):函数

人工博客

正如你所看到的,当你样品愈来愈密集,点愈来愈近,到密度时的距离从一个点到他们的邻居比像素更小,表面是彻底填充在屏幕上(见0.01)。以后,让它更密集的视觉差别,不会引发太大,你只会画的区域已经(0.01和0.001)的比较结果。
好的,如今让咱们从新定义表面函数画一个圆。有多种方法,可是我会使用这个公式:(x-x0) ^ 2 + (y-y0) ^ 2 <半径^ 2,(x0, y0)是圆的中心:

function surface(a, b) {
    var x = a * 100,
        y = b * 100,
        radius = 50,
        x0 = 50,
        y0 = 50;

    if ((x - x0) * (x - x0) + (y - y0) * (y - y0) < radius * radius) {
        // inside the circle
        return {
            x: x,
            y: y
        };
    } else {
        // outside the circle
        return null;
    }
}




if (position = surface(a, b)) {
    context.fillRect(position.x, position.y, 1, 1);
}

结果:

人工博客

就像我说的,有不一样的方法来定义一个圆,他们中的一些人不须要采样的拒绝。我将展现一个方法,可是,正如报告;我不会继续使用它在本文后面:

function surface(a, b) {
    // Circle using polar coordinates
    var angle = a * Math.PI * 2,
        radius = 50,
        x0 = 50,
        y0 = 50;

    return {
        x: Math.cos(angle) * radius * b + x0,
        y: Math.sin(angle) * radius * b + y0
    };
}

人工博客

(这个方法须要一个密度采样来填补这个比上一个圈)
好,如今让变形圆因此它看起来更像一个花瓣:

function surface(a, b) {
    var x = a * 100,
        y = b * 100,
        radius = 50,
        x0 = 50,
        y0 = 50;

    if ((x - x0) * (x - x0) + (y - y0) * (y - y0) < radius * radius) {
        return {
            x: x,
            y: y * (1 + b) / 2 // deformation
        };
    } else {
        return null;
    }
}

结果:
人工博客

好,如今这看起来更像玫瑰花瓣的形状。我建议你玩有点变形。你可使用任何你想要的数学函数,加、减、乘、除,罪恶,由于,战俘…任何东西。只是实验有点修改功能,大量的形状会出现(一些更有趣,更少)。
如今我想添加一些颜色,因此我要将颜色数据添加到表面:

function surface(a, b) {
    var x = a * 100,
        y = b * 100,
        radius = 50,
        x0 = 50,
        y0 = 50;

    if ((x - x0) * (x - x0) + (y - y0) * (y - y0) < radius * radius) {
        return {
            x: x,
            y: y * (1 + b) / 2,
            r: 100 + Math.floor((1 - b) * 155), // this will add a gradient
            g: 50,
            b: 50
        };
    } else {
        return null;
    }
}

for (a = 0; a < 1; a += .01) {
    for (b = 0; b < 1; b += .001) {
        if (point = surface(a, b)) {
            context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")";
            context.fillRect(point.x, point.y, 1, 1);
        }
    }
}

结果:
人工博客

3.3 3D曲面和透视投影

定义3d曲面很简单:只需向surface函数添加一个z属性

function surface(a, b) {
    var angle = a * Math.PI * 2,
        radius = 100,
        length = 400;

    return {
        x: Math.cos(angle) * radius,
        y: Math.sin(angle) * radius,
        z: b * length - length / 2, // by subtracting length/2 I have centered the tube at (0, 0, 0)
        r: 0,
        g: Math.floor(b * 255),
        b: 0
    };
}

如今,添加透视投影,首先咱们必须定义一个相机:

结果:
人工博客

我将个人相机放在(0,0,cameraZ),我将调用“透视图”的距离,从相机到画布。我将考虑个人画布在x/y平面上,以(0,0,cameraZ + perspective)为中心。如今,每一个采样点将被投影到画布:

var pX, pY,  // projected on canvas x and y coordinates
    perspective = 350,
    halfHeight = canvas.height / 2,
    halfWidth = canvas.width / 2,
    cameraZ = -700;

for (a = 0; a < 1; a += .001) {
    for (b = 0; b < 1; b += .01) {
        if (point = surface(a, b)) {
            pX = (point.x * perspective) / (point.z - cameraZ) + halfWidth;
            pY = (point.y * perspective) / (point.z - cameraZ) + halfHeight;
            context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")";
            context.fillRect(pX, pY, 1, 1);
        }
    }
}

结果以下:
人工博客

3.4 Z-buffer

z-buffer是计算机图形学中很常见的一种技术,它能够在距离摄像机较近的点上绘制距离摄像机较远的点。它的工做原理是保持一个数组与每像素画近z的图像。

人工博客

这是可视化的z缓冲的玫瑰,与黑色的相机远,白色接近它。
实现:

var zBuffer = [],
    zBufferIndex;

for (a = 0; a < 1; a += .001) {
    for (b = 0; b < 1; b += .01) {
        if (point = surface(a, b)) {
            pX = Math.floor((point.x * perspective) / (point.z - cameraZ) + halfWidth);
            pY = Math.floor((point.y * perspective) / (point.z - cameraZ) + halfHeight);
            zBufferIndex = pY * canvas.width + pX;
            if ((typeof zBuffer[zBufferIndex] === "undefined") || (point.z < zBuffer[zBufferIndex])) {
                zBuffer[zBufferIndex] = point.z;
                context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")";
                context.fillRect(pX, pY, 1, 1);
            }
        }
    }
}

3.5旋转这个圆柱体

你可使用任何向量旋转方法。对于玫瑰,我使用了欧拉旋转。让咱们实现一个绕Y轴的旋转:

function surface(a, b) {
    var angle = a * Math.PI * 2,
        radius = 100,
        length = 400,
        x = Math.cos(angle) * radius,
        y = Math.sin(angle) * radius,
        z = b * length - length / 2,
        yAxisRotationAngle = -.4, // in radians!
        rotatedX = x * Math.cos(yAxisRotationAngle) + z * Math.sin(yAxisRotationAngle),
        rotatedZ = x * -Math.sin(yAxisRotationAngle) + z * Math.cos(yAxisRotationAngle);

    return {
        x: rotatedX,
        y: y,
        z: rotatedZ,
        r: 0,
        g: Math.floor(b * 255),
        b: 0
    };
}

人工博客

3.6 蒙特卡洛取样

我在文章中使用了基于时间间隔的抽样。它须要为每一个表面设置一个适当的间隔。若是间隔很大,渲染速度会很快,但最终会在表面留下一些没有填充的洞。另外一方面,若是间隔过短,则呈现增量的时间会达到没法接受的数量。
那么,让咱们切换到蒙特卡罗抽样:

var i;

window.setInterval(function () {
    for (i = 0; i < 10000; i++) {
        if (point = surface(Math.random(), Math.random())) {
            pX = Math.floor((point.x * perspective) / (point.z - cameraZ) + halfWidth);
            pY = Math.floor((point.y * perspective) / (point.z - cameraZ) + halfHeight);
            zBufferIndex = pY * canvas.width + pX;
            if ((typeof zBuffer[zBufferIndex] === "undefined") || (point.z < zBuffer[zBufferIndex])) {
                zBuffer[zBufferIndex] = point.z;
                context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")";
                context.fillRect(pX, pY, 1, 1);
            }
        }
    }
}, 0);

如今,a和b参数被设置为两个随机值。采样足够的点,表面就会以这种方式彻底填充。我每次画10000个点而后让屏幕根据间隔更新。

另外,只有在伪随机数发生器质量良好的状况下,才能保证曲面的彻底填充。在一些浏览器中,数学。随机是用一个线性同余发生器实现的,这可能会致使一些曲面的问题。若是你须要一个好的PRNG采样,你可使用高质量的像Mersenne Twister(它有JS实现),或者在一些浏览器中可用的加密随机生成器。使用低差别序列也是很是明智的。

人工博客

四、总结

完成玫瑰,玫瑰的每一部分,每个表面,都是同时呈现的。我为函数添加了第三个参数,该函数选择玫瑰的部分来返回一个点。数学上它是一个分段函数,每一块都表明玫瑰的一部分。在花瓣的例子中,我使用旋转和拉伸/变形来建立全部的花瓣。全部的工做都是经过混合本文中暴露的概念来完成的。

虽然经过采样显式表面是一种很是著名的方法,也是最古老的3d图形方法之一,但个人分段/蒙特卡罗/z-buffer方法可能不多像我这样用于艺术目的。虽然不是很是具备创新性,在实际场景中也不是颇有用,可是它很是适合js1k的环境,在这种环境中,简单性和最小的大小都是须要的。

经过这篇文章,我真的但愿可以激励那些对计算机图形感兴趣的读者去尝试和享受不一样的渲染方法。在图形领域有一个完整的世界,研究和使用它是使人惊奇的。


版权声明:本文为人工博客的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处连接及本声明。
本文连接:https://www.94rg.com/article/1724

相关文章
相关标签/搜索