1. 程式人生 > >JavaScript-WebGL2學習筆記六 - 裝置座標下拾取物體

JavaScript-WebGL2學習筆記六 - 裝置座標下拾取物體

Date: 2018-4-03  
Author: kagula  
Description:  
        實現物件的拾取比較複雜,所以打算分兩步來理解, 先做二維座標裡的物件拾取。
        這裡演示WebGL NDC(Native Device Coordinator)座標系統下,物件拾取功能。
        *一個紅色和綠色距形代表被拾取物件。
        *藍色距形表示游標位置。
        *被拾取物件在游標下面,就會被render成白色。

這個演示由index.html shader.js shape.js webgl_helper.js四個檔案組成

index.html

<html>
<head>
    <!--
        Title: JavaScript-WebGL2學習筆記六 - 裝置座標下拾取物體
        Date: 2018-4-03  
        Author: kagula  
  
        Description:  
        實現物件的拾取比較複雜,所以打算分兩步來做, 先做二維座標裡的物件拾取。
        演示WebGL NDC(Native Device Coordinator)座標系統下,物件拾取功能。
        *一個紅色和綠色距形代表被拾取物件。
        *藍色距形表示游標位置。
        *被拾取物件在游標下面,就會被render成白色。

        拾取原理:
        採用了frame buffer object的辦法,  把object id當作pixel, render到看不見的螢幕上,
        然後在這個螢幕上檢查當前游標下是否存在object id, 如果有, 就說明游標在這個object id所代表的物件上.
        
  
        Reference: 
        [1]《WebGL(陸) texParameteri引數說明》
        https://www.web-tinker.com/article/20163.html
  
        Run environment  
        [1]Chrome 65.0.3325.162  
        [2]nginx  1.12.2  
  
        Remark
    -->  

    <title>JavaScript-WebGL2學習筆記六 - 裝置座標下拾取物體</title>

    <meta charset="utf-8">
    <!-- gl-matrix version 2.4.0 from http://glmatrix.net/ -->
    <script type="text/javascript" src="gl-matrix-min.js"></script>

    <script type="text/javascript" src="webgl_helper.js"></script>
    <script type="text/javascript" src="shader.js"></script>
    <script type="text/javascript" src="shape.js"></script>
</head>

<body>
    <canvas id="glCanvas" width="320" height="200"></canvas>
</body>

</html>

<script>
    const g_canvas = document.querySelector("#glCanvas");
    var gl;

    main();
    
    function main() {
        gl = g_canvas.getContext("webgl2", {stencil: true});    

        if (!gl) {
            alert("Unable to initialize WebGL2. Your browser or machine may not support it.");
            return;
        }        

        var contextAttributes = gl.getContextAttributes();
        if (!contextAttributes.stencil) {
            alert("Unable to support stencil.");
            return;
        }      

        initProgram(gl);
        initProgram2(gl);
        initScene(gl);
        initTextureFramebuffer(g_canvas.width, g_canvas.height) 
        drawPickObj(gl);
    }//main

    function handleMouseMove(event) {
        //client座標轉NDC(native device coordinate)座標
        var newX = event.clientX;
        var newY = event.clientY;

        var x = ( newX / g_canvas.width ) * 2 - 1;
        var y = - ( newY / g_canvas.height ) * 2 + 1;
        //console.log("x=" + x + ",y=" + y);

        //
        drawCursorLocation(gl ,x ,y ,newX ,newY);
    }

    g_canvas.onmousemove  = handleMouseMove;
</script>

shader.js

var programInfo;
var programInfo2;

function initShaderProgram(gl, vsSource, fsSource) {
    const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
    const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);

    // Create the shader program
    const shaderProgram = gl.createProgram();
    gl.attachShader(shaderProgram, vertexShader);
    gl.attachShader(shaderProgram, fragmentShader);
    gl.linkProgram(shaderProgram);

    // If creating the shader program failed, alert
    if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
        alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
        return null;
    }

    return shaderProgram;
}

function setShaderProgramArrayArg(gl, destPositionInShader, srcArray, elementSize)
{
    gl.bindBuffer(gl.ARRAY_BUFFER, srcArray);

    gl.vertexAttribPointer(
        destPositionInShader,
        elementSize,// pull out 2 values per iteration //Must be 1, 2, 3, or 4.
        gl.FLOAT,// the data in the buffer is 32bit floats
        false,// don't normalize
        0,//stride, how many bytes to get from one set of values to the next
        0);//how many bytes inside the buffer to start from

    gl.enableVertexAttribArray(destPositionInShader);    
}

function loadShader(gl, type, source) {
    const shader = gl.createShader(type);

    gl.shaderSource(shader, source);
    gl.compileShader(shader);

    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
    }

    return shader;
}

function initProgram(gl)
{
    const vsSource = `
    attribute vec4 aVertexPosition;
    attribute vec4 aVertexColor;

    varying lowp vec4 vColor;

    void main() {
      gl_Position = aVertexPosition;
      vColor = aVertexColor;
    }
    `;

    const fsSource = `
    precision highp float;
    varying lowp vec4 vColor;
    uniform float isReturnWhite;
    void main() {
        if(isReturnWhite > .0)
          gl_FragColor = vec4(isReturnWhite, isReturnWhite, isReturnWhite, 1);
        else
          gl_FragColor = vColor;
    }
    `;
    
    const shaderProgram = initShaderProgram(gl, vsSource, fsSource);

    programInfo = {
        program: shaderProgram,
        attribLocations: {
            vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
            vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'),
        },    
        uniformLocations: {    
            isReturnWhite: gl.getUniformLocation(shaderProgram, 'isReturnWhite')
        }
    };
}


function initProgram2(gl)
{
    const vsSource = `
    attribute vec4 aVertexPosition;

    void main() {
      gl_Position = aVertexPosition;
    }
    `;

    const fsSource = `
    precision highp float;

    uniform float highByte;
    uniform float lowByte;
    void main() {
      gl_FragColor = vec4(0, 0, highByte, lowByte );
    }
    `;
    
    const shaderProgram = initShaderProgram(gl, vsSource, fsSource);

    programInfo2 = {
        program: shaderProgram,
        attribLocations: {
            vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition')
        },    
        uniformLocations: {    
            highByte: gl.getUniformLocation(shaderProgram, 'highByte'),
            lowByte: gl.getUniformLocation(shaderProgram, 'lowByte')
        }
    };
}

shape.js

function createRedShape(gl) {
    const positionBuffer = gl.createBuffer();//不用的時候可以通過gl.deleteBuffer(buffer);刪除
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    const positions = [
        0.5, 0.5,
        -0.5, 0.5,
        0.5, -0.5,
        -0.5, -0.5,
    ];
    gl.bufferData(gl.ARRAY_BUFFER,
                    new Float32Array(positions),
                    gl.STATIC_DRAW);
    positionBuffer.itemSize = 2;


    const colors = [
    1.0, 0.0, 0.0, 1.0,    // 
    1.0, 0.0, 0.0, 1.0,    // red
    1.0, 0.0, 0.0, 1.0,    // 
    1.0, 0.0, 0.0, 1.0,    // 
    ];
    const colorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    colorBuffer.itemSize = 4;

    return {
        position: positionBuffer,
        color: colorBuffer,
        objectId:1001
    };
}

function createGreenShape(gl) {
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    const positions = [
        0.6, 0.5,
        0, 0.5,
        0.6, -0.5,
        0, -0.5,
    ];
    gl.bufferData(gl.ARRAY_BUFFER,
                    new Float32Array(positions),
                    gl.STATIC_DRAW);
    positionBuffer.itemSize = 2;

    const colors = [
        0.0, 1.0, 0.0, 1.0,    // 
        0.0, 1.0, 0.0, 1.0,    // 
        0.0, 1.0, 0.0, 1.0,    // 
        0.0, 1.0, 0.0, 1.0,    // 
    ];
    const colorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    colorBuffer.itemSize = 4;

    return {
        position: positionBuffer,
        color: colorBuffer,
        objectId:1002
    };
}

function createCursorShape(gl, x, y, cursorSize)
{
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

    let left = x - cursorSize;
    let right = x + cursorSize;
    let top = y + cursorSize;
    let bottom = y - cursorSize; 
    const positions = [
        right, top,
        left, top,
        right, bottom,
        left, bottom
    ];
    gl.bufferData(gl.ARRAY_BUFFER,
        new Float32Array(positions),
        gl.STATIC_DRAW);
    positionBuffer.itemSize = 2;

    const colors = [
        0.0, 0.0, 1.0, 1.0,    // 
        0.0, 0.0, 1.0, 1.0,    // 
        0.0, 0.0, 1.0, 1.0,    // 
        0.0, 0.0, 1.0, 1.0,    // 
    ];
    const colorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    colorBuffer.itemSize = 4;

    return {
        position: positionBuffer,
        color: colorBuffer,
    };
}

webgl_helper.js

var g_redShape;
var g_greenShape;
var g_cursorShape;

function initScene(gl)
{
    g_redShape = createRedShape(gl);
    g_greenShape = createGreenShape(gl);    
}

function drawPickObj(gl) {
    drawBackground(gl);
    
    drawRedShape(gl,0);
    drawGreenShape(gl,0);
}

function drawCursor(gl, x, y)
{
    if(g_cursorShape!=null)
    {
        gl.deleteBuffer(g_cursorShape.positionBuffer);
        gl.deleteBuffer(g_cursorShape.colorBuffer);
    }

    var cursorSize = 0.06;
    g_cursorShape = createCursorShape(gl, x, y, cursorSize);

    setShaderProgramArrayArg(gl,
        programInfo.attribLocations.vertexPosition, 
        g_cursorShape.position, g_cursorShape.position.itemSize);
        
    setShaderProgramArrayArg(gl,
        programInfo.attribLocations.vertexColor, 
        g_cursorShape.color, g_cursorShape.color.itemSize);
        
    gl.useProgram(programInfo.program);
    gl.uniform1f(programInfo.uniformLocations.isReturnWhite, 0);  
        
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}

function getObjectIdUnderCursor(gl, screenX, screenY, cursorSize)
{
    //bind to FBO
    gl.bindFramebuffer(gl.FRAMEBUFFER, rttFramebuffer);  

    //
    gl.clearColor(0.0, 0.0, 0.0, 0.0);
    gl.clearDepth(1.0);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.depthFunc(gl.LEQUAL);
    gl.enable(gl.DEPTH_TEST);
    gl.enable(gl.CULL_FACE);
    gl.disable(gl.BLEND);

    //red shape
    {
        var lowByte = g_redShape.objectId & 255;
        var highByte = (g_redShape.objectId >>> 8) & 255;
        lowByte = lowByte/255;//值域轉為[0,1]
        highByte = highByte/255;//值域轉為[0,1]
    
        setShaderProgramArrayArg(gl,
            programInfo2.attribLocations.vertexPosition, 
            g_redShape.position, g_redShape.position.itemSize);
    
        gl.useProgram(programInfo2.program);  
        gl.uniform1f(programInfo2.uniformLocations.lowByte, lowByte);  
        gl.uniform1f(programInfo2.uniformLocations.highByte, highByte);  
    
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 
    }

    //green shape
    {
        var lowByte = g_greenShape.objectId & 255;
        var highByte = (g_greenShape.objectId >>> 8) & 255;
        lowByte = lowByte/255;//值域轉為[0,1]
        highByte = highByte/255;//值域轉為[0,1]
    
        setShaderProgramArrayArg(gl,
            programInfo2.attribLocations.vertexPosition, 
            g_greenShape.position, g_greenShape.position.itemSize);
    
        gl.uniform1f(programInfo2.uniformLocations.lowByte, lowByte);  
        gl.uniform1f(programInfo2.uniformLocations.highByte, highByte);  
    
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 
    }

    //gl.flush(); 

    //
    var objId = 0;
    var pixelData = new Uint8Array(4 * (cursorSize*2) * (cursorSize*2));
    gl.readPixels(screenX - cursorSize, screenY - cursorSize, 
        cursorSize*2, cursorSize*2, gl.RGBA, gl.UNSIGNED_BYTE, pixelData);

    if (pixelData && pixelData.length) {
        var length = (cursorSize*2) * (cursorSize*2);
        for(var index = 0; index < length; index += 4)
        {
            objId = pixelData[index + 3];//shader中的定義域[0,1],取出來的值域[0,255],所以不需要再multiply 255.
            objId += 256 * pixelData[index + 2];
            if(objId!=0)
            {
                //console.log("objId = " + objId);
                break;
            }//if
        }//for
    }//if

    //unbind
    gl.bindFramebuffer(gl.FRAMEBUFFER, null); 

    return objId;    
}//getObjectIdUnderCursor

function drawBackground(gl)
{
    gl.clearColor(0.0, 0.0, 0.0, 1.0);  // Clear to black, fully opaque
    gl.clearDepth(1.0);                 // Clear everything
    gl.clearStencil(0);                 // 用0填充 stencil buffer
    gl.enable(gl.DEPTH_TEST);           // Enable depth testing
    gl.depthFunc(gl.LEQUAL);            // Near things obscure far things

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);    
}

function drawGreenShape(gl, isHighlight)
{
    setShaderProgramArrayArg(gl,
        programInfo.attribLocations.vertexPosition, 
        g_greenShape.position, g_greenShape.position.itemSize);
        
    setShaderProgramArrayArg(gl,
        programInfo.attribLocations.vertexColor, 
        g_greenShape.color, g_greenShape.color.itemSize);        
        
    gl.useProgram(programInfo.program);
    gl.uniform1f(programInfo.uniformLocations.isReturnWhite, isHighlight);  

    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 
}

function drawRedShape(gl, isHighlight)
{
    setShaderProgramArrayArg(gl,
        programInfo.attribLocations.vertexPosition, 
        g_redShape.position, g_redShape.position.itemSize);
        
    setShaderProgramArrayArg(gl,
        programInfo.attribLocations.vertexColor, 
        g_redShape.color, g_redShape.color.itemSize);
        
    gl.useProgram(programInfo.program);
    gl.uniform1f(programInfo.uniformLocations.isReturnWhite, isHighlight);  

    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 
}

//這裡draw layout設定不合理, 雖然不影響演示pick object的實現,等有時間,再完善demo.
function drawCursorLocation(gl, x, y, screenX, screenY)
{
    drawPickObj(gl);

    var objectId = getObjectIdUnderCursor(gl,screenX, screenY, 4);
    if( objectId != 0 )
    {
        if(objectId == g_redShape.objectId)
        {
            drawBackground(gl);
            drawRedShape(gl,1);
            drawGreenShape(gl,0);
        } else if(objectId == g_greenShape.objectId){
            drawBackground(gl);
            drawRedShape(gl,0);
            drawGreenShape(gl,1);
        }
    }    
    
    //這裡cursor size的設定不是很好, 雖然不影響演示pick object的實現,等有時間,再完善demo.
    drawCursor(gl,x,y);
}

var rttFramebuffer;  
var rttTexture;  
  
function initTextureFramebuffer(canvasWidth,canvasHeight) {  
    //create frame buffer  
    rttFramebuffer = gl.createFramebuffer();  
    gl.bindFramebuffer(gl.FRAMEBUFFER, rttFramebuffer);  
    rttFramebuffer.width = canvasWidth;  
    rttFramebuffer.height = canvasHeight;  
  
    //create texture  
    rttTexture = gl.createTexture();  
    gl.bindTexture(gl.TEXTURE_2D, rttTexture);  
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);  
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);  
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    //gl.generateMipmap(gl.TEXTURE_2D);//如果texture的width和height,不符合width=height=2^n等式generate mip map會失敗。  
  
    //把texture的圖片資料指標登出(交給frame buffer管理)  
    gl.texImage2D(gl.TEXTURE_2D, //指定目標紋理,這個值必須是gl.TEXTURE_2D  
    0, // 執行細節級別。0是最基本的影象級別,n表示第N級貼圖細化級別  
    gl.RGBA, //internalFormat, 指定紋理中的顏色元件。可選的值有GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE, GL_LUMINANCE_ALPHA 等幾種。  
    rttFramebuffer.width, rttFramebuffer.height, //紋理影象的寬、高度,必須是2的n次方。紋理圖片至少要支援64個材質元素的寬、高度  
    0, //邊框的寬度。必須為0。  
    gl.RGBA, //源資料的顏色格式, 不需要和internalFormat取值必須相同。  
    gl.UNSIGNED_BYTE, //源資料分量的資料型別。  
    null);//記憶體中指向影象資料的指標  
  
    //create render buffer  
    var renderbuffer = gl.createRenderbuffer();  
    gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);  
    //設定當前工作的渲染緩衝的儲存大小  
    gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, //這兩個引數是固定的  
    rttFramebuffer.width, rttFramebuffer.height);  
  
    //texture繫結到frame buffer中  
    //https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/framebufferTexture2D  
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,   
    gl.TEXTURE_2D, //target  
    rttTexture, //source, A WebGLTexture object whose image to attach.  
    0);//A GLint specifying the mipmap level of the texture image to be attached. Must be 0.  
  
  
    //把render buffer繫結到frame buffer上  
    gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT,   
    gl.RENDERBUFFER, //renderBufferTarget, A GLenum specifying the binding point (target) for the render buffer.  
    renderbuffer);//A WebGLRenderbuffer object to attach.  
  
    //unbind  
    gl.bindTexture(gl.TEXTURE_2D, null);  
    gl.bindRenderbuffer(gl.RENDERBUFFER, null);  
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);  
}