Android OpenGL ES 2.0,为每个三角形计算法线

Android OpenGL ES 2.0, Compute Normal for each triangle

我需要在 Android 中使用 Opengl ES 2.0 为每个三角形面(而不是每个顶点)计算法线。但是我不能直接在 Fragment Shader 中传递属性。

找到一个解决方案:为每个三角形重复顶点,并将三角形面法线作为属性传递给顶点着色器

但我不想复制顶点。我正在使用 Vertex Indices.

绘制三角形

所以,一个顶点被多个三角形共享,那么我应该如何计算三角形面的法线。

p.s。我是opengl的新手。

最简单的解决方案就是复制顶点。顶点着色器很少成为瓶颈。我不知道您的具体需求,但是,在某些情况下,复制顶点并不是一个好的解决方案。例如,如果网格被蒙皮和动画化,这意味着大量计算发生在顶点着色器中。另一种情况是当网格在顶点着色器中以某种奇怪的方式进行动画处理时,您必须重新计算法线。显然,您无法在顶点着色器中计算每个面的法线。您可以在几何着色器中执行此操作,但我们在 OpenGL ES 2.0 中没有。然而,有一个简单的解决方案——在片段着色器中计算法线!所以,如果顶点复制对你不起作用,这里是解决方案:

  1. 我们需要一个 OpenGL 扩展 - standard_derivatives,它得到了广泛的支持,但您仍然需要在 运行 代码之前检查设备是否支持它.要启用扩展,您必须在代码之前将以下行添加到片段着色器:

    #extension GL_OES_standard_derivatives : enable
    
  2. 我们需要一个可变变量来表示世界坐标中的顶点位置。它应该在顶点着色器中计算,它是如何完成的很大程度上取决于你的着色器。它用于许多需求,因此您可能已经在顶点着色器中计算了它。所以让我们假设,我们在片段着色器中有这一行:

    varying vec3 positionWorld;
    
  3. 我们需要相机的视图矩阵。您可能已经将一个传递给片段着色器。假设我们在片段着色器中有这个制服:

    uniform mat4 viewMatrix;
    
  4. 现在,我们要计算法线。首先,我们计算视图 space 中的法线,然后转换为世界 space。为了计算视图中的法线space,我们使用导数函数:

    vec3 normalViewSpace = normalize(cross(dFdx(positionWorldSpace), dFdy(positionWorldSpace)));
    

    此处位置的导数是相对于屏幕中的 x 和 y 坐标 space。这意味着我们有两个位于表面平面中的向量。为了使表面正常,我们做叉积。当然,结果不是单位向量,所以我们还需要对其进行归一化。

  5. 最后一步是计算世界中的法线space。视图矩阵应用从世界 space 到视图 space 的转换。人们可能会认为我们需要计算它的逆矩阵,因为我们需要从视图 space 到世界 space,但是因为视图矩阵是标准正交的,所以该矩阵的转置也是它的逆矩阵,所以代码将是:

    vec3 normalWorldSpace = (vec4(normalViewSpace, 0.0) * viewMatrix).xyz;
    
  6. 为了让生活更轻松,我们可以将所有内容包装到一个函数中:

    vec3 ReconstructNormal(vec3 positionWorldSpace) 
    {
        vec3 normalViewSpace = normalize(cross(dFdx(positionWorldSpace), dFdy(positionWorldSpace)));
        vec3 normalWorldSpace = (vec4(normalViewSpace, 0.0) * viewMatrix).xyz;
        return normalWorldSpace;
    }
    

现在我们在世界 space 中有了重建的法线。下面,只是一个简单的例子,为什么这会非常有用。请注意,由于它使用 WebGL,因此它也几乎与 OpenGL ES 2.0 兼容。

var container;
var camera, scene, renderer;
var mesh;
var uniforms;

var clock = new THREE.Clock();

init();
animate();

function init() {
    container = document.getElementById('container');

    camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 100);
    camera.position.z = 0.6;
    camera.position.y = 0.2;
    camera.rotation.x = -0.45;

    scene = new THREE.Scene();

    var boxGeometry = new THREE.PlaneGeometry(0.75, 0.75, 32, 32);
     
    var heightMap = THREE.ImageUtils.loadTexture("data:image/png;base64,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");
         
    heightMap.wrapT = heightMap.wrapS = THREE.RepeatWrapping;

    uniforms = {u_time: {type: "f", value: 0.0 }, u_heightMap: {type: "t",value:heightMap} };

    var material = new THREE.ShaderMaterial({
    uniforms: uniforms,
        side: THREE.DoubleSide, 
        wireframe: false,
        vertexShader: document.getElementById('vertexShader').textContent,
        fragmentShader: document.getElementById('fragment_shader').textContent
    });


    mesh = new THREE.Mesh(boxGeometry, material);
    mesh.rotation.x = 3.14 / 2.0;
    scene.add(mesh);

    renderer = new THREE.WebGLRenderer();
    renderer.setClearColor( 0x000000, 1 );
    container.appendChild(renderer.domElement);

    onWindowResize();

    window.addEventListener('resize', onWindowResize, false);
}

function onWindowResize(event) {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}

function animate() {
    requestAnimationFrame(animate);
    render();
}

function render() {
    var delta = clock.getDelta();
    uniforms.u_time.value += delta;
    //mesh.rotation.z += delta * 0.5;
    renderer.render(scene, camera);
}
    body { margin: 0px; overflow: hidden; }
<script src="https://threejs.org/build/three.min.js"></script>
<div id="container"></div>

<script id="fragment_shader" type="x-shader/x-fragment">
    #extension GL_OES_standard_derivatives : enable
    
    varying vec3 positionWorld; // position of vertex in world coordinates
    
    vec3 ReconstructNormal(vec3 positionWorldSpace) 
    {
        vec3 normalViewSpace = normalize(cross(dFdx(positionWorldSpace), dFdy(positionWorldSpace)));
        vec3 normalWorldSpace = (vec4(normalViewSpace, 0.0) * viewMatrix).xyz;
        return normalWorldSpace;
    }

    // Just some example of using a normal. Here we do a really simple shading
    void main( void ) 
    {
        vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
        vec3 normal = ReconstructNormal(positionWorld);
        float diffuse = max(dot(lightDir, normal), 0.0);
        vec3 albedo = vec3(0.2, 0.4, 0.7);
        gl_FragColor = vec4(albedo * diffuse, 1.0);    
    }
</script>

<script id="vertexShader" type="x-shader/x-vertex">
    uniform lowp sampler2D u_heightMap;
    uniform float u_time;
    
    varying vec3 positionWorld;
                
    // Example of vertex shader that moves vertices
    void main()
    {
        vec3 pos = position;
        vec2 offset1 = vec2(1.0, 0.5) * u_time * 0.01;
        vec2 offset2 = vec2(0.5, 1.0) * u_time * 0.01;
        float hight1 = texture2D(u_heightMap, uv + offset1).r * 0.02;
        float hight2 = texture2D(u_heightMap, uv + offset2).r * 0.02;
        pos.z += hight1 + hight2; 
        vec4 mvPosition = modelViewMatrix * vec4( pos, 1.0 );
        positionWorld = mvPosition.xyz;
        gl_Position = projectionMatrix * mvPosition;
    }
</script>