Javascript 3d Terrain Without Three.js

谁说我不能喝 提交于 2019-12-12 05:19:02

问题


I have searched around but I can't find anything like what I'm trying to do that doesn't use Three.js in some way (I can't use Three.js because my computer is too old to support Webgl). Here's what I've got so far:

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;
    }
}

Currently, the result looks like a Christmas tree but I want it to look more like actual 3d wireframe terrain.


回答1:


3D wire frame basics

3D can be done on any systems that can move pixels. Thought not by dedicated hardware Javascript can do alright if you are after simple 3d.

This answers shows how to create a mesh, rotate and move it, create a camera and move it, and project the whole lot onto the 2D canvas using simple moveTo, and lineTo calls.

This answer is a real rush job so apologies for the typos (if any) and messy code. Will clean it up in the come few days (if time permits). Any questions please do ask in the comments.

Update I have not done any basic 3D for some time so having a little fun I have added to the answer with more comments in the code and added some extra functionality.

  • vec3 now has normalise, dot, cross functions.
  • mat now has lookat function and is ready for much more if needed.
  • mesh now maintains its own world matrix
  • Added box, and line that create box and line meshs
  • Created a second vector type vec3S (S for simple) that is just coordinates no functionality
  • Demo now shows how to add more objects, position them in the scene, use a lookat transform

Details about the code.

The code below is the basics of 3D. It has a mesh object to create objects out of 3D points (vertices) connected via lines.

Simple transformation for rotating, moving and scaling a model so it can be placed in the scene.

A very very basic camera that can only look forward, move up,down, left,right, in and out. And the focal length can be changed.

Only for lines as there is no depth sorting.

The demo does not clip to the camera front plane, but rather just ignores lines that have any part behind the camera;

You will have to work out the rest from the comments, 3D is a big subject and any one of the features is worth a question / answer all its own.

Oh and coordinates in 3D are origin in center of canvas. Y positive down, x positive right, and z positive into the screen. projection is basic so when you have perspective set to 400 than a object at 400 units out from camera will have a one to one match with pixel size.

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>


来源:https://stackoverflow.com/questions/44466783/javascript-3d-terrain-without-three-js

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!