Canvas旋转星场

27
我将采用以下方法来在屏幕上制作星空动画,但接下来的部分卡住了。
JS
var c = document.getElementById('stars'),
    ctx = c.getContext("2d"),
    t = 0; // time

c.width = 300;
c.height = 300;

var w = c.width,
    h = c.height,
    z = c.height,
    v = Math.PI; // angle of vision

(function animate() {

    Math.seedrandom('bg');
    ctx.globalAlpha = 1;

    for (var i = 0; i <= 100; i++) {

        var x = Math.floor(Math.random() * w), // pos x
            y = Math.floor(Math.random() * h), // pos y
            r = Math.random()*2 + 1, // radius
            a = Math.random()*0.5 + 0.5, // alpha

            // linear
            d = (r*a),       // depth
            p = t*d;         // pixels per t

        x = x - p;       // movement
        x = x - w * Math.floor(x / w); // go around when x < 0

        (function draw(x,y) {
            var gradient = ctx.createRadialGradient(x, y, 0, x + r, y + r, r * 2);
            gradient.addColorStop(0, 'rgba(255, 255, 255, ' + a + ')');
            gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');

            ctx.beginPath();
            ctx.arc(x, y, r, 0, 2*Math.PI);
            ctx.fillStyle = gradient;
            ctx.fill();

            return draw;

        })(x, y);

    }

    ctx.restore();
    t += 1;

    requestAnimationFrame(function() {
        ctx.clearRect(0, 0, c.width, c.height);
        animate();
    });
})();

HTML

<canvas id="stars"></canvas>

CSS

canvas {
    background: black;
}

JSFiddle

目前它所做的是使用考虑星星的不透明度和大小的 delta X 来动画每个星星,因此最小的星星看起来移动较慢。

使用 p = t; 使所有星星以相同的速度移动。

问题

我正在寻找一个明确定义的模型,其中速度给人们一种星星围绕观察者旋转的错觉,定义为旋转中心 cX,cY 和视角 v 的角度,这是可以看到的 2π 的什么部分(如果圆的中心不是屏幕中心,则半径应至少为最大部分)。我正在努力找到一种方法,即使对于旋转 π 的中心圆也将该余弦应用于星运动的速度。

这些图解可能会进一步解释我的要求:

居中圆:

center of vision in x,y

非中心:

shifted center

不同视角:

different angle of vision

我真的不知道该如何前进。我已经有些慌了。您能否帮助我迈出一些第一步吗?

谢谢。


更新

我已经在这段代码中取得了一些进展:

        // linear
        d = (r*a)*z,   // depth
        v = (2*Math.PI)/w,
        p = Math.floor( d * Math.cos( t * v ) );     // pixels per t

    x = x + p;       // movement
    x = x - w * Math.floor(x / w); // go around when x < 0

JSFiddle

这里,p是一个粒子在均匀圆周运动中的x坐标,v是角速度。但这会产生钟摆效应。我不确定如何更改这些方程以创造观察者转动的幻象。


更新 2:

快成功了。Freenode频道##Math的一位用户很友好地建议使用以下计算方法:

        // linear
        d = (r*a),       // depth
        p = t*d;         // pixels per t

    x = x - p;       // movement
    x = x - w * Math.floor(x / w); // go around when x < 0

    x = (x / w) - 0.5;
    y = (y / h) - 0.5;

    y /= Math.cos(x);

    x = (x + 0.5) * w;
    y = (y + 0.5) * h;

JSFiddle

这个实现效果上看起来不错,但是在变量方面没有一个明确定义的模型(它只是“hack”了这个效果),因此我无法看到实现不同实现的简单方法(更改中心,视角)。真正的模型可能与这个非常相似。


更新3

根据Iftah的回复,我能够使用Sylvester将旋转矩阵应用于需要先保存在数组中的星星。现在每颗星星的z坐标已确定,半径r和不透明度a也是由它派生出来的。代码有很大的不同和长度,所以我没有发布它,但这可能是朝着正确方向迈出的一步。我还无法使其持续旋转。在每帧上使用矩阵操作似乎在性能方面代价高昂。

JSFiddle


1
把你的星空看作天空,把你的观察者想象成从他们的窗户望出去的星星。让你的星空比视口更大(比用户的窗口更大)。例如,当cY向下移动时,您将显示更多的顶部和较少的底部星空。您没有提到cZ(就像在您的第二个插图中,人们用眼睛贴在视口上观看)。当cZ接近视口时,您会在所有方向上显示更多的星空。这些星星距离如此遥远,以至于它们各自的速度不需要改变——除非您处于超光速! - markE
不过,将星星从画布上绘制出来是不必要的。我认为我们正在寻找的是 p = f(d),它使用余弦函数来模拟在截面 v 的圆中 x 的变化。正如你所指出的那样,有一个 cZ 变量在第一个示例中我没有放置,我会添加它。 - Alain Jacomet Forte
正确,画布元素是您的视口,它显示了更大的星场的一部分(就像在画布“窗口”之外)。您只需要显示在画布元素内部的星场部分即可。 - markE
2个回答

13
这里有一些伪代码可以完成你所说的功能。
Make a bunch of stars not too far but not too close (via rejection sampling)
Set up a projection matrix (defines the camera frustum)
Each frame
    Compute our camera rotation angle
    Make a "view" matrix (repositions the stars to be relative to our view)
    Compose the view and projection matrix into the view-projection matrix
    For each star
        Apply the view-projection matrix to give screen star coordinates
        If the star is behind the camera skip it
        Do some math to give the star a nice seeming 'size'
        Scale the star coordinate to the canvas
        Draw the star with its canvas coordinate and size

我已经完成了上述内容的实现。它使用gl-matrix JavaScript库来处理一些矩阵数学运算。这是很好的东西。(可以在此处找到此示例的Fiddle:http://jsfiddle.net/fpft4176/11/,也可以在下面查看。)

var c = document.getElementById('c');
var n = c.getContext('2d');

// View matrix, defines where you're looking
var viewMtx = mat4.create();

// Projection matrix, defines how the view maps onto the screen
var projMtx = mat4.create();

// Adapted from https://dev59.com/XWMl5IYBdhLWcg3wbGl1
function ComputeProjMtx(field_of_view, aspect_ratio, near_dist, far_dist, left_handed) {
    // We'll assume input parameters are sane.
    field_of_view = field_of_view * Math.PI / 180.0; // Convert degrees to radians
    var frustum_depth = far_dist - near_dist;
    var one_over_depth = 1 / frustum_depth;
    var e11 = 1.0 / Math.tan(0.5 * field_of_view);
    var e00 = (left_handed ? 1 : -1) * e11 / aspect_ratio;
    var e22 = far_dist * one_over_depth;
    var e32 = (-far_dist * near_dist) * one_over_depth;
    return [
        e00, 0, 0, 0,
        0, e11, 0, 0,
        0, 0, e22, e32,
        0, 0, 1, 0
    ];
}

// Make a view matrix with a simple rotation about the Y axis (up-down axis)
function ComputeViewMtx(angle) {
    angle = angle * Math.PI / 180.0; // Convert degrees to radians
    return [
        Math.cos(angle), 0, Math.sin(angle), 0,
        0, 1, 0, 0,
        -Math.sin(angle), 0, Math.cos(angle), 0,
        0, 0, 0, 1
    ];
}

projMtx = ComputeProjMtx(70, c.width / c.height, 1, 200, true);

var angle = 0;

var viewProjMtx = mat4.create();

var minDist = 100;
var maxDist = 1000;

function Star() {
    var d = 0;
    do {
        // Create random points in a cube.. but not too close.
        this.x = Math.random() * maxDist - (maxDist / 2);
        this.y = Math.random() * maxDist - (maxDist / 2);
        this.z = Math.random() * maxDist - (maxDist / 2);
        var d = this.x * this.x +
                this.y * this.y +
                this.z * this.z;
    } while (
         d > maxDist * maxDist / 4 || d < minDist * minDist
    );
    this.dist = Math.sqrt(d);
}

Star.prototype.AsVector = function() {
    return [this.x, this.y, this.z, 1];
}

var stars = [];
for (var i = 0; i < 5000; i++) stars.push(new Star());

var lastLoop = Date.now();

function loop() {
    
    var now = Date.now();
    var dt = (now - lastLoop) / 1000.0;
    lastLoop = now;
    
    angle += 30.0 * dt;

    viewMtx = ComputeViewMtx(angle);
    
    //console.log('---');
    //console.log(projMtx);
    //console.log(viewMtx);
    
    mat4.multiply(viewProjMtx, projMtx, viewMtx);
    //console.log(viewProjMtx);
    
    n.beginPath();
    n.rect(0, 0, c.width, c.height);
    n.closePath();
    n.fillStyle = '#000';
    n.fill();
    
    n.fillStyle = '#fff';
    
    var v = vec4.create();
    for (var i = 0; i < stars.length; i++) {
        var star = stars[i];
        vec4.transformMat4(v, star.AsVector(), viewProjMtx);
        v[0] /= v[3];
        v[1] /= v[3];
        v[2] /= v[3];
        //v[3] /= v[3];
        
        if (v[3] < 0) continue;

        var x = (v[0] * 0.5 + 0.5) * c.width;
        var y = (v[1] * 0.5 + 0.5) * c.height;
        
        // Compute a visual size...
        // This assumes all stars are the same size.
        // It also doesn't scale with canvas size well -- we'd have to take more into account.
        var s = 300 / star.dist;
        
        
        n.beginPath();
        n.arc(x, y, s, 0, Math.PI * 2);
        //n.rect(x, y, s, s);
        n.closePath();
        n.fill();
    }
    
    window.requestAnimationFrame(loop);
}

loop();
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.3.1/gl-matrix-min.js"></script>
<canvas id="c" width="500" height="500"></canvas>

一些链接:

更新

这是另一个带有键盘控制的版本。很有趣。你可以通过横向移动来看到旋转和视差之间的区别。最好全屏查看。(此处的示例代码在这里,或者下面也有。)

var c = document.getElementById('c');
var n = c.getContext('2d');

// View matrix, defines where you're looking
var viewMtx = mat4.create();

// Projection matrix, defines how the view maps onto the screen
var projMtx = mat4.create();

// Adapted from https://dev59.com/XWMl5IYBdhLWcg3wbGl1
function ComputeProjMtx(field_of_view, aspect_ratio, near_dist, far_dist, left_handed) {
    // We'll assume input parameters are sane.
    field_of_view = field_of_view * Math.PI / 180.0; // Convert degrees to radians
    var frustum_depth = far_dist - near_dist;
    var one_over_depth = 1 / frustum_depth;
    var e11 = 1.0 / Math.tan(0.5 * field_of_view);
    var e00 = (left_handed ? 1 : -1) * e11 / aspect_ratio;
    var e22 = far_dist * one_over_depth;
    var e32 = (-far_dist * near_dist) * one_over_depth;
    return [
        e00, 0, 0, 0,
        0, e11, 0, 0,
        0, 0, e22, e32,
        0, 0, 1, 0
    ];
}

// Make a view matrix with a simple rotation about the Y axis (up-down axis)
function ComputeViewMtx(angle) {
    angle = angle * Math.PI / 180.0; // Convert degrees to radians
    return [
        Math.cos(angle), 0, Math.sin(angle), 0,
        0, 1, 0, 0,
        -Math.sin(angle), 0, Math.cos(angle), 0,
        0, 0, -250, 1
    ];
}

projMtx = ComputeProjMtx(70, c.width / c.height, 1, 200, true);

var angle = 0;

var viewProjMtx = mat4.create();

var minDist = 100;
var maxDist = 1000;

function Star() {
    var d = 0;
    do {
        // Create random points in a cube.. but not too close.
        this.x = Math.random() * maxDist - (maxDist / 2);
        this.y = Math.random() * maxDist - (maxDist / 2);
        this.z = Math.random() * maxDist - (maxDist / 2);
        var d = this.x * this.x +
                this.y * this.y +
                this.z * this.z;
    } while (
         d > maxDist * maxDist / 4 || d < minDist * minDist
    );
    this.dist = 100;
}

Star.prototype.AsVector = function() {
    return [this.x, this.y, this.z, 1];
}

var stars = [];
for (var i = 0; i < 5000; i++) stars.push(new Star());

var lastLoop = Date.now();


var dir = {
    up: 0,
    down: 1,
    left: 2,
    right: 3
};

var dirStates = [false, false, false, false];
var shiftKey = false;

var moveSpeed = 100.0;
var turnSpeed = 1.0;

function loop() {
    var now = Date.now();
    var dt = (now - lastLoop) / 1000.0;
    lastLoop = now;
    
    angle += 30.0 * dt;

    //viewMtx = ComputeViewMtx(angle);
    var tf = mat4.create();
    if (dirStates[dir.up]) mat4.translate(tf, tf, [0, 0, moveSpeed * dt]);
    if (dirStates[dir.down]) mat4.translate(tf, tf, [0, 0, -moveSpeed * dt]);
    if (dirStates[dir.left])
        if (shiftKey) mat4.rotate(tf, tf, -turnSpeed * dt, [0, 1, 0]);
        else mat4.translate(tf, tf, [moveSpeed * dt, 0, 0]);
    if (dirStates[dir.right])
        if (shiftKey) mat4.rotate(tf, tf, turnSpeed * dt, [0, 1, 0]);
        else mat4.translate(tf, tf, [-moveSpeed * dt, 0, 0]);
    mat4.multiply(viewMtx, tf, viewMtx);
    
    //console.log('---');
    //console.log(projMtx);
    //console.log(viewMtx);
    
    mat4.multiply(viewProjMtx, projMtx, viewMtx);
    //console.log(viewProjMtx);
    
    n.beginPath();
    n.rect(0, 0, c.width, c.height);
    n.closePath();
    n.fillStyle = '#000';
    n.fill();
    
    n.fillStyle = '#fff';
    
    var v = vec4.create();
    for (var i = 0; i < stars.length; i++) {
        var star = stars[i];
        vec4.transformMat4(v, star.AsVector(), viewProjMtx);
        
        if (v[3] < 0) continue;
        
        var d = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
        
        v[0] /= v[3];
        v[1] /= v[3];
        v[2] /= v[3];
        //v[3] /= v[3];
        

        var x = (v[0] * 0.5 + 0.5) * c.width;
        var y = (v[1] * 0.5 + 0.5) * c.height;
        
        // Compute a visual size...
        // This assumes all stars are the same size.
        // It also doesn't scale with canvas size well -- we'd have to take more into account.
        var s = 300 / d;
        
        
        n.beginPath();
        n.arc(x, y, s, 0, Math.PI * 2);
        //n.rect(x, y, s, s);
        n.closePath();
        n.fill();
    }
    
    window.requestAnimationFrame(loop);
}

loop();

function keyToDir(evt) {
    var d = -1;
    if (evt.keyCode === 38) d = dir.up
    else if (evt.keyCode === 37) d = dir.left;
    else if (evt.keyCode === 39) d = dir.right;
    else if (evt.keyCode === 40) d = dir.down;
    return d;
}

window.onkeydown = function(evt) {
    var d = keyToDir(evt);
    if (d >= 0) dirStates[d] = true;
    if (evt.keyCode === 16) shiftKey = true;
}

window.onkeyup = function(evt) {
    var d = keyToDir(evt);
    if (d >= 0) dirStates[d] = false;
    if (evt.keyCode === 16) shiftKey = false;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.3.1/gl-matrix-min.js"></script>
<div>Click in this pane. Use up/down/left/right, hold shift + left/right to rotate.</div>
<canvas id="c" width="500" height="500"></canvas>

更新2

Alain Jacomet Forte问道:

您推荐创建通用的3D方法是什么,如果您建议在矩阵级别上工作还是不要,在特定情况下可能会更好。

关于矩阵:如果您在任何平台上从头开始编写引擎,则无法避免地需要使用矩阵,因为它们有助于将基本的3D数学推广化。即使您使用OpenGL/WebGL或Direct3D,您仍然需要制作视图和投影矩阵以及用于更复杂目的的其他矩阵。 (处理法线贴图、对齐世界对象、蒙皮等...)

关于创建通用3D的方法...不要。它会运行缓慢,并且没有经过大量工作就不能高效。依赖硬件加速库来完成重活。为特定项目创建有限的3D引擎很有趣且富有启发性(例如,我想在我的网页上展示一个酷炫的动画),但是当涉及到将像素放在屏幕上以进行任何真正的操作时,出于性能目的,您希望尽可能让硬件处理。

不幸的是,Web还没有一个伟大的标准,但它正在使用WebGL中 - 学习WebGL,使用WebGL。它运行良好,并且在支持时工作良好。(但是您可以使用CSS 3D变换和Javascript轻松完成很多操作。)

如果您正在进行桌面编程,则强烈推荐使用OpenGL通过SDL(我还没有完全接受SFML) - 它是跨平台且得到很好的支持。

如果您正在编写移动电话,则OpenGL ES几乎是您唯一的选择(除了一种狗慢的软件渲染器)。

如果您想完成工作而不是从头开始编写自己的引擎,则Web的事实标准是Three.js(我发现它有效但平庸)。如果您想要完整的游戏引擎,现在有一些免费选项,主要的商业选项是Unity和Unreal。Irrlicht已经存在很长时间了-尽管我从未有机会使用它,但我听说它很好。

但是,如果您想从头开始制作所有3D内容...我总是发现Quake中的软件渲染器如何制作是一个很好的案例研究。这里可以找到其中一些资料


这基本上就是Iftah提出的建议。这是“3D”操作的标准方式。建立一个世界空间,在该空间中放置物品,并确定如果在该空间中有虚拟摄像机,则它们应该出现在屏幕上的位置。 - Kaganar
从理论上讲,星星的移动速度应该根据它们的距离而异。但是,如果星星围绕Y轴旋转,而相机位于Y轴中心,这看起来就像星星静止不动,相机在旋转。在这种情况下,没有星星会比其他星星移动得更快或更慢——就像你转动头部一样。如果您想实现更具逼真度的视差效果,则星星需要以不同的方式移动。 - Kaganar
1
请问您能否将fiddle的代码编辑到您的答案中,以便它可以永久存档,并且在SO上内联运行脚本,而不是使用fiddle rot(https://blog.stackexchange.com/2014/09/introducing-runnable-javascript-css-and-html-code-snippets/)? - Paul
非常感谢您提供这个精彩的答案,我一直在深入研究。我有一个小请求,如果您能在答案中添加您推荐的创建通用3D的方法以及是否建议在矩阵级别上工作,特别是针对这种情况。 - Alain Jacomet Forte
1
@Kaganar,你曾经问过如何使代码部分默认折叠 - 如果你仍然有兴趣这样做,你可以通过将帖子中的片段标记从<!-- begin snippet: js hide: false -->更改为<!-- begin snippet: js hide: true -->来实现。 - Maximillian Laumeister
显示剩余6条评论

2
您每帧都在重置星星的2D位置,然后移动星星(取决于每颗星星的时间和速度)- 这是实现目标的不好方法。正如您发现的那样,当您尝试将此解决方案扩展到更多场景时,它变得非常复杂。
更好的方法是仅在初始化时设置星星的3D位置,然后每帧移动一个“相机”(取决于时间)。当您想要渲染2D图像时,然后计算屏幕上的星星位置。屏幕上的位置取决于星星的3D位置和当前相机位置。 这将允许您在任何方向上移动相机,旋转相机(到任何角度),并呈现正确的星星位置,并保持您的理智。

你能提供一些你所建议的代码示例吗?这似乎是一个非常大的改变。此外,通过推断该模型,我们可以找到适合我的方程式,合理吧?-- 我不明白这在代码方面会有什么不同。似乎只是在概念上有所不同。谢谢。 - Alain Jacomet Forte
我想我建议的是构建一个小型3D引擎,这需要一些(非常少量的)线性代数来优雅地完成。 - Iftah
这是一个有趣的方法。如果您能详细说明一下,那就太好了。 - Alain Jacomet Forte
这是我按照你的评论所做的,使用numeric.js获取[x、y、z]向量和绕Y轴旋转矩阵的点积:http://jsfiddle.net/u4Lq8fgd/。但是我对线性代数不太熟悉,它们在旋转时还在移动!你能帮忙吗? - Alain Jacomet Forte
我已经在问题中使用矩阵更新了我的最后一次尝试。 - Alain Jacomet Forte
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接