没有Three.js的Javascript 3d Terrain

时间:2017-06-09 21:13:59

标签: javascript html5-canvas terrain

我已经四处寻找,但我找不到任何类似于我正在尝试做的事情,不会以某种方式使用Three.js(我不能使用Three.js因为我的电脑太旧而无法支持WebGL的)。这是我到目前为止所得到的:

HTML:

<!DOCTYPE html>
<html>
<head>
    <script type="text/javascript" src="terrain.js"></script>
    <title>Terrain</title>
</head>
<body>
<canvas id="canvas" height="400" width="400"></canvas>
</body>
</html>

使用Javascript:

var canvas, ctx, row1 = [], row2 = [], intensity = 15, width = 20, height = 20, centery = 200, centerx = 200, minus, delta = 1.6, nu = .02;

window.onload = function() {
    canvas = document.getElementById('canvas'), ctx = canvas.getContext('2d');
    ctx.lineStyle = '#000'
    for (var i = 0; i < height; i++) {
        row2 = [];
        minus = 200
        for (var j = 0; j < width; j++) {
            row2[j] = {
                x: centerx - (minus * (delta * (nu * i))),
                y: Math.floor(Math.random() * intensity) + (height * i)
            }
            minus -= height;
        }
        ctx.beginPath();
        ctx.moveTo(row2[0].x,row2[0].y)
        for (var k = 1; k < row2.length; k++) {
            ctx.lineTo(row2[k].x,row2[k].y)
            if (k == row2.length) {ctx.clostPath()}
        }
        ctx.stroke();
        if (row1[0] && row2[0]) {
            for (var l = 0; l < row2.length; l++) {
                ctx.beginPath();
                ctx.moveTo(row2[l].x,row2[l].y)
                ctx.lineTo(row1[l].x,row1[l].y)
                ctx.closePath();
                ctx.stroke();
            }
        }
        row1 = row2;
    }
}

目前,结果看起来像圣诞树,但我希望它看起来更像实际的3D线框地形。

1 个答案:

答案 0 :(得分:0)

3D线框基础

3D可以在任何可以移动像素的系统上完成。不考虑专用硬件如果您使用的是简单的3d,Javascript就可以了。

此答案显示如何创建网格,旋转和移动网格,创建相机并移动它,并使用简单的moveTo和lineTo调用将整个项目投影到2D画布上。

这个答案是一个真正的紧急工作,所以为拼写错误(如果有的话)和凌乱的代码道歉。将在几天内(如果时间允许)清理它。有任何问题请在评论中提出。

<强>更新 我有一段时间没有做任何基本的3D,所以有一点乐趣我已经在代码中添加了更多注释并添加了一些额外的功能。

  • vec3现在有规范化,点,交叉功能。
  • mat现在具有lookat功能,如果需要可以做更多准备。
  • mesh现在维护着自己的世界矩阵
  • 添加了框和创建框线和网格线的线
  • 创建了第二个矢量类型vec3S(S表示简单),它只是坐标无功能
  • 演示现在演示如何添加更多对象,将它们放置在场景中,使用外观变换

有关代码的详细信息。

以下代码是3D的基础知识。它有一个网格对象,可以通过线条连接3D点(顶点)。

旋转,移动和缩放模型的简单转换,以便将其放置在场景中。

非常非常基本的相机,只能向前看,向上,向下,向左,向右,向内和向外移动。焦距可以改变。

仅适用于行,因为没有深度排序。

演示不会剪切到相机前端,而只是忽略了相机背后有任何部分的线条;

你必须从评论中解决剩下的问题,3D是一个很大的主题,任何一个功能都值得一个问题/答案。

哦,3D中的坐标是画布中心的原点。 Y正向下,x正向右,z正向进入屏幕。投影是基本的,因此当你将透视图设置为400时,相机在距离相机400个单位时将与像素尺寸一对一匹配。

var ctx = canvas.getContext("2d");
// some usage of vecs does not need the added functionality
// and will use the basic version
const vec3Basic = { x : 0, y : 0, z: 0};
const vec3Def = {
    // Sets the vector scalars
    // Has two signatures
    // setVal(x,y,z) sets vector to {x,y,z}
    // setVal(vec) set this vector to vec
    setVal(x,y = x.y,z = x.z + (x = x.x) * 0){
        this.x = x;
        this.y = y;
        this.z = z;
    },
    // subtract v from this vector
    // Has two signatures
    // setVal(v) subtract v from this returning a new vec3
    // setVal(v,vec) subtract v from this returning result in retVec
    sub(v,retVec = vec3()){
        retVec.x = this.x - v.x;
        retVec.y = this.y - v.y;
        retVec.z = this.z - v.z;
        return retVec;
    },
    // Cross product of two vectors this and v.
    // Cross product can be thought of as get the vector
    // that is perpendicular to the plane described by the two vector we are crossing
    // Has two signatures
    // cross(vec); // returns a new vec3 as the cross product of this and vec
    // cross(vec, retVec); // set retVec as the cross product
    cross (v, retVec = vec3()){
       retVec.x = this.y * v.z - this.z * v.y;
       retVec.y = this.z * v.x - this.x * v.z;
       retVec.z = this.x * v.y - this.y * v.x;
       return retVec;
    },
    // Dot product
    // Dot product of two vectors if both normalized can be thought of as finding the cos of the angle
    // between two vectors. If not normalised the dot product will give you < 0 if v points away from
    // the plane that this vector is perpendicular to, if > 0 the v points in the same direction as the
    // plane perpendicular to this vector. if 0 then v is at 90 degs to the plane this is perpendicular to
    // Using vector dot on its self is the same as getting the length squared
    // dot(vec3); // returns a number as a float
    dot (v){ return this.x * v.x + this.y * v.y + this.z * this.z },
    // normalize normalizes a vector. A normalized vector has length equale to 1 unit
    // Has two signitures
    // normalise(); normalises this vector returning this
    // normalize(retVec); normalises this vector but puts the normalised vector in retVec returning 
    //                    returning retVec. Thiis is unchanged.
    normalize(retVec = this){
        // could have used len = this.dot(this) but for speed all functions will do calcs internaly
        const len = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
        // it is assumed that all vector are valid (have length) so no test is made to avoid
        // the divide by zero that will happen for invalid vectors.
        retVec.x = this.x / len;
        retVec.y = this.y / len;
        retVec.z = this.z / len;
    }
}
// Created as a singleton to close over working constants
const matDef = (()=>{
    // to seed up vector math the following closed over vectors are used
    // rather than create and dispose of vectors for every operation needing them
    // Currently not used
    const V1 = vec3();
    return {
        // The matrix is just 3 pointers one for each axis
        // They represent the direction and scale in 3D of each axis 
        // when you transform a point x,y,z you move x along the x axis, 
        // then y along y and z along the z axis        
        xAxis : null,
        yAxis : null,
        zAxis : null,
        // this is a position x,y,z and represents where in 3D space an objects
        // center coordinate (0,0,0) will be. It is simply added to a point
        // after it has been moved along the 3 axis.
        pos : null,
        // This function does most of the 3D work in most 3D environments.
        // It rotates, scales, translates, and a whole lot more.
        // It is a cut down of the full 4 by 4 3D matrix you will find in 
        // Libraries like three.js 
        transformVec3(vec,retVec = {}){
            retVec.x = vec.x * this.xAxis.x + vec.y * this.yAxis.x + vec.z * this.zAxis.x + this.pos.x;
            retVec.y = vec.x * this.xAxis.y + vec.y * this.yAxis.y + vec.z * this.zAxis.y + this.pos.y;
            retVec.z = vec.x * this.xAxis.z + vec.y * this.yAxis.z + vec.z * this.zAxis.z + this.pos.z;
            return retVec;
        },
        // resets the matrix
        identity(){  // default matrix
            this.xAxis.setVal(1,0,0); // x 1 unit long in the x direction
            this.yAxis.setVal(0,1,0); // y 1 unit long in the y direction
            this.zAxis.setVal(0,0,1); // z 1 unit long in the z direction
            this.pos.setVal(0,0,0);   // and position at the origin.
            
        },
        init(){  // need to call this before using due to the way I create these
                 // objects.
            this.xAxis = vec3(1,0,0);
            this.yAxis = vec3(0,1,0);
            this.zAxis = vec3(0,0,1);
            this.pos = vec3(0,0,0);
            return this; // must have this line for the constructor function to return 
        },
        setRotateY(amount){
            var x = Math.cos(amount);
            var y = Math.sin(amount);
            this.xAxis.x = x;
            this.xAxis.y = 0;
            this.xAxis.z = y;
            this.zAxis.x = -y;
            this.zAxis.y = 0;
            this.zAxis.z = x;
        },
        // creates a look at transform from the current position
        // point is a vec3.
        // No check is made to see if look at is at pos which will invalidate this matrix
        // Note scale is lost in this operation.
        lookAt(point){
            // zAxis along vector from pos to point
            this.pos.sub(point,this.zAxis).normalize();
            // use y as vertical reference
            this.yAxis.x = 0;
            this.yAxis.y = 1; 
            this.yAxis.z = 0;
            // get x axis perpendicular to the plane described by z and y axis
            // need to normalise as z and y axis may not be at 90 deg
            this.yAxis.cross(this.zAxis,this.xAxis).normalize();
            // Get the y axis that is perpendicular to z and x axis
            // Normalise is not really needed but rounding errors can be problematic
            // so the normalise just fixes some of the rounding errors.
            this.zAxis.cross(this.xAxis,this.yAxis).normalize();
        },      
            
    }
})();
// Mesh object has buffers for the 
// model as verts
// transformed mesh as tVerts
// projected 2D verts as dVerts (d for display)
// An a array of lines. Each line has two indexes that point to the 
// vert that define their ends.
// Buffers are all preallocated to stop GC slowing everything down.
const meshDef = {
    addVert(vec){
        this.verts.push(vec);
        // vec3(vec) in next line makes a copy of the vec. This is important
        // as using the same vert in the two buffers will result in strange happenings.        
        this.tVerts.push(vec3S(vec)); // transformed verts pre allocated so GC does not bite
        this.dVerts.push({x:0,y:0}); // preallocated memory for displaying 2d projection
                                     // when x and y are zero this means that it is not visible
        return this.verts.length - 1;
    },
    addLine(index1,index2){
        this.lines.push(index1,index2);
    },
    transform(matrix = this.matrix){
        for(var i = 0; i < this.verts.length; i++){
            matrix.transformVec3(this.verts[i],this.tVerts[i]);
        }
    },    
    eachVert(callback){
        for(var i = 0; i < this.verts.length; i++){
            callback(this.tVerts[i],i);
        }
    },
    eachLine(callback){
        for(var i = 0; i < this.lines.length; i+= 2){
            var ind1 = this.lines[i];
            var v1 = this.dVerts[ind1]; // get the start 
            if(v1.x !== 0 && v1.y !== 0){ // is valid
                var ind2 = this.lines[i+ 1]; // get end of line
                var v2 = this.dVerts[ind2]; 
                if(v2.x !== 0 && v2.y !== 0){ // is valid
                    callback(v1,v2);
                }
            }
        }
    },
    init(){ // need to call this befor using
        this.verts = [];
        this.lines = [];
        this.dVerts = []; 
        this.tVerts = [];
        this.matrix = mat();
        return this; // must have this line for the construtor function to return 
    }    
}
const cameraDef = {
    projectMesh(mesh){ // create a 2D mesh
        mesh.eachVert((vert,i)=>{
            var z = (vert.z + this.position.z);
            if(z < 0){  // is behind the camera then ignor it
                mesh.dVerts[i].x = mesh.dVerts[i].y = 0;
            }else{
                var s =  this.perspective / z;
                mesh.dVerts[i].x = (vert.x + this.position.x) * s;
                mesh.dVerts[i].y = (vert.y + this.position.y) * s;
            }
        })  
    },
    drawMesh(mesh){  // renders the 2D mesh
        ctx.beginPath();
        mesh.eachLine((v1,v2)=>{
            ctx.moveTo(v1.x,v1.y);
            ctx.lineTo(v2.x,v2.y);
        })
        ctx.stroke();
    }
}
// vec3S creates a basic (simple) vector
// 3 signatures
//vec3S(); // return vec 1,0,0
//vec3S(vec); // returns copy of vec
//vec3S(x,y,z); // returns {x,y,z}
function vec3S(x = {x:1,y:0,z:0},y = x.y ,z = x.z + (x = x.x) * 0){ // a 3d point
    return Object.assign({},vec3Basic,{x, y, z});
}
// vec3S creates a basic (simple) vector
// 3 signatures
//vec3S(); // return vec 1,0,0
//vec3S(vec); // returns copy of vec
//vec3S(x,y,z); // returns {x,y,z}
function vec3(x = {x:1,y:0,z:0},y = x.y ,z = x.z + (x = x.x) * 0){ // a 3d point
    return Object.assign({},vec3Def,{x,y,z});
}
function mat(){ // matrix used to rotate scale and move a 3d point
    return Object.assign({},matDef).init();
}
function mesh(){  // this is for storing objects as points in 3d and lines conecting points
    return Object.assign({},meshDef).init();
}
function camera(perspective,position){  // this is for displaying 3D
    return Object.assign({},cameraDef,{perspective,position});
}
// grid is the number of grids x,z and size is the overal size for x
function createLandMesh(gridx,gridz,size,maxHeight){ 
    var m = mesh(); // create a mesh
    var hs = size/2 ; 
    var step = size / gridx;
    for(var z = 0; z < gridz; z ++){
        for(var x = 0; x < gridx; x ++){
            // create a vertex. Y is random 
            m.addVert(vec3S(x * step - hs, (Math.random() * maxHeight), z * step-hs)); // create a vert
        }
    }
    for(var z = 0; z < gridz-1; z ++){
        for(var x = 0; x < gridx-1; x ++){
            if(x < gridx -1){ // dont go past end
                m.addLine(x + z * gridx,x + 1 + z * gridx); // add line across
            }
            if(z < gridz - 1){  // dont go past end
                m.addLine(x + z * (gridx-1),x + 1 + (z + 1) * (gridx-1));
            }
        }
    }
    return m;
}
function createBoxMesh(size){
    var s = size / 2;
    var m = mesh(); // create a mesh
    // add bottom
    m.addVert(vec3S(-s,-s,-s));
    m.addVert(vec3S( s,-s,-s));
    m.addVert(vec3S( s, s,-s));
    m.addVert(vec3S(-s, s,-s));
    // add top verts
    m.addVert(vec3S(-s,-s, s));
    m.addVert(vec3S( s,-s, s));
    m.addVert(vec3S( s, s, s));
    m.addVert(vec3S(-s, s, s));
    // add lines
    /// bottom lines
    m.addLine(0,1);
    m.addLine(1,2);
    m.addLine(2,3);
    m.addLine(3,0);
    /// top lines
    m.addLine(4,5);
    m.addLine(5,6);
    m.addLine(6,7);
    m.addLine(7,4);
    // side lines
    m.addLine(0,4);
    m.addLine(1,5);
    m.addLine(2,6);
    m.addLine(3,7);
    return m;
    
}
function createLineMesh(v1 = vec3S(),v2 = vec3S()){
    const m = mesh();
    m.addVert(v1);
    m.addVert(v2);
    m.addLine(0,1);
    return m;
}
//Create a land mesh grid 20 by 20 and 400 units by 400 units in size
var land = createLandMesh(20,20,400,20);  // create a land mesh
var box = createBoxMesh(50);
var box1 = createBoxMesh(25);
var line = createLineMesh(); // line conecting boxes
line.tVerts[0] = box.matrix.pos; // set the line transformed tVect[0] to box matrix.pos
line.tVerts[1] = box1.matrix.pos; // set the line transformed tVect[0] to box1 matrix.pos
var cam = camera(200,vec3(0,0,0)); // create a projection with focal len 200 and at 0,0,0
box.matrix.pos.setVal(0,-100,400);
box1.matrix.pos.setVal(0,-100,400);
land.matrix.pos.setVal(0,100,300); // move down 100, move away 300


var w = canvas.width;
var h = canvas.height;
var cw = w / 2;  // center of canvas
var ch = h / 2;



function update(timer){
    // next section just maintains canvas size and resets state and clears display 
    if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
        cw = (w = canvas.width = innerWidth) /2;
        ch = (h = canvas.height = innerHeight) /2;
    }
    ctx.setTransform(1,0,0,1,0,0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
    ctx.fillStyle = "black";
    ctx.fillRect(0,0,canvas.width,canvas.height);
    // end of standard canvas maintenance 
    
    // render from center of canvas by setting canvas origin to center
    ctx.setTransform(1,0,0,1,canvas.width / 2,canvas.height / 2)


    land.matrix.setRotateY(timer/1000); // set matrix to rotation position
    land.transform();
    // move the blue box
    var t = timer/1000;
    box1.matrix.pos.setVal(Math.sin(t / 2.1) * 100,Math.sin( t / 3.2) * 100, Math.sin(t /5.3) * 90+300);
    // Make the cyan box look at the blue box
    box.matrix.lookAt(box1.matrix.pos);
    // Transform boxes from local to world space
    box1.transform();
    box.transform();

    
    // set camera x,y pos to mouse pos;
    cam.position.x = mouse.x - cw;
    cam.position.y = mouse.y - ch;
    
    // move in and out
    if (mouse.buttonRaw === 1) { cam.position.z -= 1 }
    if (mouse.buttonRaw === 4) {cam.position.z += 1 }
    
    // Converts mesh transformed verts to 2D screen coordinates
    cam.projectMesh(land);
    cam.projectMesh(box);
    cam.projectMesh(box1);
    cam.projectMesh(line);
    
    // Draw each mesh in turn
    ctx.strokeStyle = "#0F0";
    cam.drawMesh(land);
    ctx.strokeStyle = "#0FF";
    cam.drawMesh(box);
    ctx.strokeStyle = "#00F";
    cam.drawMesh(box1);
    ctx.strokeStyle = "#F00";
    cam.drawMesh(line);

    
    ctx.setTransform(1,0,0,1,cw,ch / 4);
    ctx.font = "20px arial";
    ctx.textAlign = "center";
    ctx.fillStyle = "yellow";
    ctx.fillText("Move mouse to move camera. Left right mouse move in out",0,0)

    requestAnimationFrame(update);
}
requestAnimationFrame(update);


// A mouse handler from old lib of mine just to give some interaction
// not needed for the 3d
var mouse = (function () {
    var m; // alias for mouse
    var mouse = {
        x : 0, y : 0, // mouse position
        buttonRaw : 0,                      
        buttonOnMasks : [0b1, 0b10, 0b100],  // mouse button on masks
        buttonOffMasks : [0b110, 0b101, 0b011], // mouse button off masks
        bounds : null,
        event(e) {
            m.bounds = m.element.getBoundingClientRect();
            m.x = e.pageX - m.bounds.left - scrollX;
            m.y = e.pageY - m.bounds.top - scrollY;
            if (e.type === "mousedown") { m.buttonRaw |= m.buttonOnMasks[e.which - 1] }
            else if (e.type === "mouseup") { m.buttonRaw &= m.buttonOffMasks[e.which - 1] }
            e.preventDefault();
        },
        start(element) {
            m.element = element === undefined ? document : element;
            "mousemove,mousedown,mouseup".split(",").forEach(name =>  document.addEventListener(name, mouse.event) );
            document.addEventListener("contextmenu", (e) => { e.preventDefault() }, false);
            return mouse;
        },
    }
    m = mouse;
    return mouse;
})().start(canvas);
canvas { position:absolute; top : 0px; left : 0px;}
<canvas id="canvas"></canvas>