JavaScript中如何转换成半精度浮点数?

16

我想在WebGL中使用OES_texture_half_float扩展并提供自己的数据,但JavaScript中没有Float16Array。那么我该如何生成半精度浮点数数据?

1个回答

18

我将这两个函数改编成了JavaScript。它们似乎可以正常工作。

  • 从这里开始

    var toHalf = (function() {
    
      var floatView = new Float32Array(1);
      var int32View = new Int32Array(floatView.buffer);
    
      /* 这个方法比OpenEXR实现更快(经常使用,例如在Ogre中),并且具有舍入的额外好处,受James Tursa的半精度代码启发。 */
      return function toHalf(val) {
    
        floatView[0] = val;
        var x = int32View[0];
    
        var bits = (x >> 16) & 0x8000; /* 获取符号 */
        var m = (x >> 12) & 0x07ff; /* 保留一个额外的位进行舍入 */
        var e = (x >> 23) & 0xff; /* 在这里使用int更快 */
    
        /* 如果为零,或者是非规格化数,或者指数下溢太多以至于成为非规格化的一半,则返回有符号的零。*/
        if (e < 103) {
          return bits;
        }
    
        /* 如果是NaN,则返回NaN。如果是Inf或指数溢出,则返回Inf。*/
        if (e > 142) {
          bits |= 0x7c00;
          /* 如果指数为0xff并且设置了一个尾数位,则表示NaN,而不是Inf,因此确保我们也设置了一个尾数位。*/
          bits |= ((e == 255) ? 0 : 1) && (x & 0x007fffff);
          return bits;
        }
    
        /* 如果指数下溢但不太多,则返回非规格化数 */
        if (e < 113) {
          m |= 0x0800;
          /* 额外的舍入可能会溢出并将尾数设置为0和指数设置为1,这是可以的。*/
          bits |= (m >> (114 - e)) + ((m >> (113 - e)) & 1);
          return bits;
        }
    
        bits |= ((e - 112) << 10) | (m >> 1);
        /* 额外的舍入。溢出将使尾数为0并增加指数,这是可以的。*/
        bits += m & 1;
        return bits;
      };
    
    }());
    
  • 从这里开始

    var toHalf = (function() {
    
      var floatView = new Float32Array(1);
      var int32View = new Int32Array(floatView.buffer);
    
      return function toHalf( fval ) {
        floatView[0] = fval;
        var fbits = int32View[0];
        var sign  = (fbits >> 16) & 0x8000;          // 只有符号
        var val   = ( fbits & 0x7fffffff ) + 0x1000; // 舍入值
    
        if( val >= 0x47800000 ) {             // 可能是或变成NaN/Inf
          if( ( fbits & 0x7fffffff ) >= 0x47800000 ) {
                                              // 是或必须成为NaN/Inf
            if( val < 0x7f800000 ) {          // 值但太大
              return sign | 0x7c00;           // 使其成为+/-Inf
            }
            return sign | 0x7c00 |            // 仍然是+/-Inf或NaN
                ( fbits & 0x007fffff ) >> 13; // 保留NaN(和Inf)位
          }
          return sign | 0x7bff;               // 未舍入的不完全Inf
        }
        if( val >= 0x38800000 ) {             // 保持规
    
    <p>使用示例</p>
    
    <pre><code>var tex = new Uint16Array(4);
    tex[0] = toHalf(0.5);
    tex[1] = toHalf(1);
    tex[2] = toHalf(123);
    tex[3] = toHalf(-13);
    

    这里有一个使用WebGL的第一个示例

    var toHalf = (function() {
    
      var floatView = new Float32Array(1);
      var int32View = new Int32Array(floatView.buffer);
    
      /* This method is faster than the OpenEXR implementation (very often
       * used, eg. in Ogre), with the additional benefit of rounding, inspired
       * by James Tursa?s half-precision code. */
      return function toHalf(val) {
    
        floatView[0] = val;
        var x = int32View[0];
    
        var bits = (x >> 16) & 0x8000; /* Get the sign */
        var m = (x >> 12) & 0x07ff; /* Keep one extra bit for rounding */
        var e = (x >> 23) & 0xff; /* Using int is faster here */
    
        /* If zero, or denormal, or exponent underflows too much for a denormal
         * half, return signed zero. */
        if (e < 103) {
          return bits;
        }
    
        /* If NaN, return NaN. If Inf or exponent overflow, return Inf. */
        if (e > 142) {
          bits |= 0x7c00;
          /* If exponent was 0xff and one mantissa bit was set, it means NaN,
                       * not Inf, so make sure we set one mantissa bit too. */
          bits |= ((e == 255) ? 0 : 1) && (x & 0x007fffff);
          return bits;
        }
    
        /* If exponent underflows but not too much, return a denormal */
        if (e < 113) {
          m |= 0x0800;
          /* Extra rounding may overflow and set mantissa to 0 and exponent
           * to 1, which is OK. */
          bits |= (m >> (114 - e)) + ((m >> (113 - e)) & 1);
          return bits;
        }
    
        bits |= ((e - 112) << 10) | (m >> 1);
        /* Extra rounding. An overflow will set mantissa to 0 and increment
         * the exponent, which is OK. */
        bits += m & 1;
        return bits;
      };
    
    }());
    
    (function() {
      twgl.setAttributePrefix("a_");
      var m4 = twgl.m4;
      var gl = document.getElementById("c").getContext("webgl");
      var ext =  gl.getExtension("OES_texture_half_float");
      if (!ext) {
        alert("no support for OES_texture_half_float on this device");
        return;
      }
      var onePointProgramInfo = twgl.createProgramInfo(gl, ["vs", "fs"]);
    
      var shapes = [
        twgl.primitives.createCubeBufferInfo(gl, 2),
      ];
    
      function rand(min, max) {
        if (max === undefined) {
          max = min;
          min = 0;
        }
        return min + Math.random() * (max - min);
      }
    
      // Shared values
      var baseHue = rand(360);
      var lightWorldPosition = [1, 8, -10];
      var lightColor = [1, 1, 1, 1];
      var camera = m4.identity();
      var view = m4.identity();
      var viewProjection = m4.identity();
    
      var halfFloatData = new Uint16Array(4);
    
      // will divide by 400 in shader to prove it works.
      halfFloatData[0] = toHalf(100);  
      halfFloatData[1] = toHalf(200);
      halfFloatData[2] = toHalf(300);
      halfFloatData[3] = toHalf(400);
    
      var textures = twgl.createTextures(gl, {
        // A 2x2 pixel texture from a JavaScript array
        checker: {
          // Note: You need OES_texture_half_float_linear to use anything other than NEAREST
          mag: gl.NEAREST,
          min: gl.NEAREST,
          format: gl.LUMINANCE,
          type: ext.HALF_FLOAT_OES,
          src: halfFloatData,
        },
      });
    
      var objects = [];
      var drawObjects = [];
      var numObjects = 100;
      for (var ii = 0; ii < numObjects; ++ii) {
        var uniforms;
        var programInfo;
        var shape;
        shape = shapes[ii % shapes.length];
        programInfo = onePointProgramInfo;
        uniforms = {
          u_diffuse: textures.checker,
          u_worldViewProjection: m4.identity(),
        };
        drawObjects.push({
          programInfo: programInfo,
          bufferInfo: shape,
          uniforms: uniforms,
        });
        objects.push({
          translation: [rand(-10, 10), rand(-10, 10), rand(-10, 10)],
          ySpeed: rand(0.1, 0.3),
          zSpeed: rand(0.1, 0.3),
          uniforms: uniforms,
        });
      }
    
      function render(time) {
        time *= 0.001;
        twgl.resizeCanvasToDisplaySize(gl.canvas);
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    
        gl.enable(gl.DEPTH_TEST);
        gl.clearColor(0.2, 0.3, 0.8, 1);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    
        var radius = 20;
        var orbitSpeed = time * 0.1;
        var projection = m4.perspective(30 * Math.PI / 180, gl.canvas.clientWidth / gl.canvas.clientHeight, 0.5, 100);
        var eye = [Math.cos(orbitSpeed) * radius, 4, Math.sin(orbitSpeed) * radius];
        var target = [0, 0, 0];
        var up = [0, 1, 0];
    
        m4.lookAt(eye, target, up, camera);
        m4.inverse(camera, view);
        m4.multiply(projection, view, viewProjection);
    
        objects.forEach(function(obj) {
          var uni = obj.uniforms;
          var world = m4.identity(world);
          m4.rotateY(world, time * obj.ySpeed, world);
          m4.rotateZ(world, time * obj.zSpeed, world);
          m4.translate(world, obj.translation, world);
          m4.rotateX(world, time, world);
          m4.multiply(viewProjection, world, uni.u_worldViewProjection);
        });
    
        twgl.drawObjectList(gl, drawObjects);
    
        requestAnimationFrame(render);
      }
      requestAnimationFrame(render);
    }());
    body {
      margin: 0;
      font-family: monospace;
    }
    canvas {
      width: 100vw;
      height: 100vh;
      display: block;
    }
    <script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
    <canvas id="c"></canvas>
    <script id="vs" type="notjs">
    uniform mat4 u_worldViewProjection;
    
    attribute vec4 a_position;
    attribute vec2 a_texcoord;
    
    varying vec4 v_position;
    varying vec2 v_texCoord;
    
    void main() {
      v_texCoord = a_texcoord;
      gl_Position = u_worldViewProjection * a_position;
    }
    </script>
    <script id="fs" type="notjs">
    precision mediump float;
    
    varying vec2 v_texCoord;
    uniform sampler2D u_diffuse;
    
    void main() {
      gl_FragColor = texture2D(u_diffuse, v_texCoord) / vec4(400.0, 400.0, 400.0, 1.0);
    }
    </script>


    请注意,如果您正在上传图像纹理,则此方法有效,但最好在离线时进行此转换。然后,您可以将它们存储为二进制文件,并使用XMLHttpRequest下载。您可以使用gzip进行压缩(与png大致相同),只要您的服务器发送正确的标题告诉浏览器文件已被gzip压缩,它就应该会自动解压缩。

  • 第一种方法和第二种方法之间有任何真正的区别吗?有没有理由更喜欢其中的一种? - Brandon
    哪种方法更快? - Stiefel
    它们在某些浏览器上的速度大约相同,误差范围为 +/- 5%。 https://jsperf.com/converting-from-floats-to-half-floats - Paul Merrill

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