Как преобразовать значение vec4 rgba в float?

я упаковал некоторые данные float в текстуру как unsigned_byte, мой единственный вариант в webgl. Теперь я хотел бы распаковать его в вершинном шейдере. Когда я пробую пиксель, я получаю vec4, который действительно является одним из моих поплавков. Как преобразовать из vec4 в float?

5 ответов


следующий код специально для iPhone 4 GPU с использованием OpenGL ES 2.0. У меня нет опыта работы с WebGL, поэтому я не могу утверждать, что знаю, как код будет работать в этом контексте. Кроме того, основная проблема здесь заключается в том, что highp float не 32 бита, а вместо 24 бит.

мое решение для шейдеров фрагментов - я не пробовал его в вершинном шейдере, но он не должен отличаться. Для использования вам нужно будет получить RGBA texel из формы sampler2d и убедиться, что что значения каждого канала R,G,B и A находятся между 0.0 и 255.0 . Этого легко добиться следующим образом:

highp vec4 rgba = texture2D(textureSamplerUniform, texcoordVarying)*255.0;

вы должны знать, что endianess вашей машины будет диктовать правильный порядок ваших байтов. Приведенный выше код предполагает, что поплавки хранятся в порядке big-endian. Если вы видите, что ваши результаты неверны, просто поменяйте порядок данных, написав

rgba.rgba=rgba.abgr;

сразу после линии, где вы установили его. Как вариант замены индексов на rgba. Я думаю, что выше линии более интуитивные, хотя и менее склонен к неосторожным ошибки. Я не уверен, работает ли он для всех заданных входных данных. Я проверил большой диапазон чисел и обнаружил, что decode32 и encode32 не являются точными инверсиями. Я также опустил код, который я использовал для его тестирования.

#pragma STDGL invariant(all) 

highp vec4 encode32(highp float f) {
    highp float e =5.0;

    highp float F = abs(f); 
    highp float Sign = step(0.0,-f);
    highp float Exponent = floor(log2(F)); 
    highp float Mantissa = (exp2(- Exponent) * F);
    Exponent = floor(log2(F) + 127.0) + floor(log2(Mantissa));
    highp vec4 rgba;
    rgba[0] = 128.0 * Sign  + floor(Exponent*exp2(-1.0));
    rgba[1] = 128.0 * mod(Exponent,2.0) + mod(floor(Mantissa*128.0),128.0);  
    rgba[2] = floor(mod(floor(Mantissa*exp2(23.0 -8.0)),exp2(8.0)));
    rgba[3] = floor(exp2(23.0)*mod(Mantissa,exp2(-15.0)));
    return rgba;
}

highp float decode32(highp vec4 rgba) {
    highp float Sign = 1.0 - step(128.0,rgba[0])*2.0;
    highp float Exponent = 2.0 * mod(rgba[0],128.0) + step(128.0,rgba[1]) - 127.0; 
    highp float Mantissa = mod(rgba[1],128.0)*65536.0 + rgba[2]*256.0 +rgba[3] + float(0x800000);
    highp float Result =  Sign * exp2(Exponent) * (Mantissa * exp2(-23.0 )); 
    return Result;
}

void main()  
{  
    highp float result;

    highp vec4 rgba=encode32(-10.01);
    result = decode32(rgba);
}

вот некоторые ссылки на точность IEEE, которые я нашел полезными. link1 и. Link2. Link3.


вы, похоже, создаете массив JavaScript чисел с плавающей запятой. Затем вы создаете Uint8Array, передавая этот массив конструктору.

согласно спецификации WebGL (или, скорее, спецификации, на которую ссылается спецификация WebGL при якобы указании этого поведения), преобразование из поплавков в неподписанные байты происходит в одном из двух способов, исходя из назначения. Если пункт назначения считается "зажатым", то он зажимает номер в диапазоне назначения, а именно [0, 255] для вашего случая. Если пункт назначения не считается "зажатым", то его принимают по модулю 28. "Спецификация" WebGL достаточно плоха, что не совсем ясно, считается ли конструкция Uint8Array зажатой или нет. Зажато или взято по модулю 28, десятичная точка отрезана и целое значение хранится.

Впрочем, когда вы даете эти данные открытьWebGL, Вы сказали WebGL интерпретировать байты как нормализованные целочисленные значения без знака. Это означает, что входные значения в диапазоне [0, 255] будут доступны пользователям текстуры как [0, 1] значения с плавающей запятой.

поэтому, если ваш входной массив имел значение 183.45, значение в Uint8Array будет 183. Значение в текстуре будет 183/255, или 0.718. Если входное значение было 0.45, Uint8Array будет содержать 0, а результат текстуры будет 0.0.

теперь, поскольку вы передали данные как GL_RGBA, это означает, что каждые 4 байта без знака будут приняты как один texel. Так что каждый звонок texture будет получать эти конкретные четыре значения (в заданной координате текстуры, используя заданные параметры фильтрации), таким образом возвращая vec4.

неясно, что вы собираетесь делать с этими данными с плавающей запятой, поэтому трудно сделать предложения о том, как лучше всего передать данные float шейдеру. Однако общим решением было бы использовать расширение OES_texture_float и фактически создать текстуру, которая хранит данные с плавающей запятой. Конечно, если он недоступен, вам все равно придется найти способ сделать то, что вы хотите.

кстати, Хронос действительно должен стыдиться себя за то, что даже назвал WebGL спецификацией. Он почти ничего не определяет, это просто куча ссылок на другое технические характеристики, что делает нахождение последствия чего-либо чрезвычайно сложно.


Twerdster опубликовал отличный код в своем ответе. Так что все заслуги на его стороне. Я публикую этот новый ответ, так как комментарии не допускают хороших синтаксических цветных блоков кода, и я хотел поделиться некоторым кодом. Но если вам нравится код, пожалуйста, upvote Twerdster оригинальный ответ.

в предыдущем посте Twerdster он упомянул, что декодирование и кодирование могут работать не для всех значений.

для дальнейшего тестирования этого и проверки результата я сделал программу java. При переносе кода я старался оставаться как можно ближе к коду шейдера (поэтому я реализовал некоторые вспомогательные функции). Примечание. Я также использую функцию store/load для сравнения того, что происходит при записи/чтении из текстуры.

я узнал, что:

  1. вам нужен специальный случай для нуля
  2. вам также может понадобиться специальный случай для бесконечности, но я не реализовал это, чтобы сохранить шейдер простым (например: быстрее)
  3. из-за ошибок округления иногда поэтому результат был неправильным:
    • вычесть 1 из показателя, когда из-за округления мантисса неправильно нормализована (например, мантисса
    • изменить float Mantissa = (exp2(- Exponent) * F); to float Mantissa = F/exp2(Exponent); уменьшить ошибки точности
    • использовать float Exponent = floor(log2(F)); к показателю calc. (упрощено новой проверкой мантиссы)

используя эти небольшие модификации, я получил равный выход почти на всех входах и получил только небольшие ошибки между оригиналом и закодированное / декодированное значение, когда что-то идет не так, в то время как в первоначальной реализации Twerdster ошибки округления часто приводили к неправильному показателю (таким образом, результат был выключен в два раза).

обратите внимание, что это тестовое приложение Java, которое я написал, чтобы проверить алгоритм. Я надеюсь, что это также будет работать при портировании на GPU. Если кто-то пытается запустить его на GPU, пожалуйста, оставьте комментарий с вашим опытом.

и для кода с простым тестом, чтобы попробовать разные числа, пока он не терпит неудачу.

import java.io.PrintStream;
import java.util.Random;

public class BitPacking {

    public static float decode32(float[] v)
    {
        float[] rgba = mult(255, v);
        float sign = 1.0f - step(128.0f,rgba[0])*2.0f;
        float exponent = 2.0f * mod(rgba[0],128.0f) + step(128.0f,rgba[1]) - 127.0f;    
        if(exponent==-127)
            return 0;           
        float mantissa = mod(rgba[1],128.0f)*65536.0f + rgba[2]*256.0f +rgba[3] + ((float)0x800000);
        return sign * exp2(exponent-23.0f) * mantissa ;     
    }   

    public static float[] encode32(float f) {           
        float F = abs(f); 
        if(F==0){
            return new float[]{0,0,0,0};
        }
        float Sign = step(0.0f,-f);
        float Exponent = floor(log2(F)); 

        float Mantissa = F/exp2(Exponent); 

        if(Mantissa < 1)
            Exponent -= 1;      

        Exponent +=  127;

        float[] rgba = new float[4];
        rgba[0] = 128.0f * Sign  + floor(Exponent*exp2(-1.0f));
        rgba[1] = 128.0f * mod(Exponent,2.0f) + mod(floor(Mantissa*128.0f),128.0f);  
        rgba[2] = floor(mod(floor(Mantissa*exp2(23.0f -8.0f)),exp2(8.0f)));
        rgba[3] = floor(exp2(23.0f)*mod(Mantissa,exp2(-15.0f)));
        return mult(1/255.0f, rgba);
    }

    //shader build-in's

    public static float exp2(float x){
        return (float) Math.pow(2, x);
    }

    public static float[] step(float edge, float[] x){
        float[] result = new float[x.length];
        for(int i=0; i<x.length; i++)
            result[i] = x[i] < edge ? 0.0f : 1.0f;
        return result;      
    }

    public static float step(float edge, float x){
        return x < edge ? 0.0f : 1.0f;
    }

    public static float mod(float x, float y){
        return x-y * floor(x/y);
    }

    public static float floor(float x){
        return (float) Math.floor(x);
    }

    public static float pow(float x, float y){
        return (float)Math.pow(x, y);
    }

    public static float log2(float x)
    {
        return (float) (Math.log(x)/Math.log(2));
    }

    public static float log10(float x)
    {
        return (float) (Math.log(x)/Math.log(10));
    }

    public static float abs(float x)
    {
        return (float)Math.abs(x);
    }   

    public static float log(float x)
    {
        return (float)Math.log(x);
    }

    public static float exponent(float x)
    {
        return floor((float)(Math.log(x)/Math.log(10)));
    }

    public static float mantissa(float x)
    {
        return floor((float)(Math.log(x)/Math.log(10)));
    }

    //shorter matrix multiplication 
    private static float[] mult(float scalar, float[] w){
        float[] result = new float[4];
        for(int i=0; i<4; i++)
            result[i] = scalar * w[i];
        return result;
    }

    //simulate storage and retrieval in 4-channel/8-bit texture 
    private static float[] load(int[] v)
    {
        return new float[]{v[0]/255f, v[1]/255f, v[2]/255f, v[3]/255f};
    }

    private static int[] store(float[] v)
    {
        return new int[]{((int) (v[0]*255))& 0xff, ((int) (v[1]*255))& 0xff, ((int) (v[2]*255))& 0xff, ((int) (v[3]*255))& 0xff};       
    }

    //testing until failure, and some specific hard-cases separately
    public static void main(String[] args) {

        //for(float v : new float[]{-2097151.0f}){ //small error here 
        for(float v : new float[]{3.4028233e+37f, 8191.9844f, 1.0f, 0.0f, 0.5f, 1.0f/3, 0.1234567890f, 2.1234567890f, -0.1234567890f, 1234.567f}){
            float output = decode32(load(store(encode32(v))));
            PrintStream stream = (v==output) ?  System.out : System.err;
            stream.println(v + " ?= " + output);
        }   

        //System.exit(0);

        Random r = new Random();
        float max = 3200000f;
        float min = -max;
        boolean error = false;
        int trials = 0;
        while(!error){
            float fin = min + r.nextFloat() * ((max - min) + 1);
            float fout = decode32(load(store(encode32(fin))));
            if(trials % 10000 == 0)
                System.out.print('.');
            if(trials % 1000000 == 0)
                System.out.println();
            if(fin != fout){
                System.out.println();
                System.out.println("correct trials = " + trials);
                System.out.println(fin + " vs " + fout);                
                error = true;
            }
            trials++;
        }       
    }
}

я попробовал решение Arjans, но оно вернуло недопустимые значения для 0, 1, 2, 4. Была ошибка с упаковкой экспоненты, которую я изменил, поэтому exp берет один 8-битный поплавок, а знак упакован с мантиссой:

//unpack a 32bit float from 4 8bit, [0;1] clamped floats
float unpackFloat4( vec4 _packed)
{
    vec4 rgba = 255.0 * _packed;
    float sign =  step(-128.0, -rgba[1]) * 2.0 - 1.0;
    float exponent = rgba[0] - 127.0;    
    if (abs(exponent + 127.0) < 0.001)
        return 0.0;           
    float mantissa =  mod(rgba[1], 128.0) * 65536.0 + rgba[2] * 256.0 + rgba[3] + (0x800000);
    return sign *  exp2(exponent-23.0) * mantissa ;     


}

//pack a 32bit float into 4 8bit, [0;1] clamped floats
vec4 packFloat(float f) 
{
    float F = abs(f); 
    if(F == 0.0)
    {
        return  vec4(0,0,0,0);
    }
    float Sign =  step(0.0, -f);
    float Exponent = floor( log2(F)); 

    float Mantissa = F/ exp2(Exponent); 
    //std::cout << "  sign: " << Sign << ", exponent: " << Exponent << ", mantissa: " << Mantissa << std::endl;
    //denormalized values if all exponent bits are zero
    if(Mantissa < 1.0)
        Exponent -= 1;      

    Exponent +=  127;

    vec4 rgba;
    rgba[0] = Exponent;
    rgba[1] = 128.0 * Sign +  mod(floor(Mantissa * float(128.0)),128.0);
    rgba[2] = floor( mod(floor(Mantissa* exp2(float(23.0 - 8.0))), exp2(8.0)));
    rgba[3] = floor( exp2(23.0)* mod(Mantissa, exp2(-15.0)));
    return (1 / 255.0) * rgba;
}

вы не сможете просто интерпретировать 4 байта без знака как биты значения float (которое, я полагаю, вы хотите) в шейдере (по крайней мере, не в GLES или WebGL, я думаю). То, что вы можете сделать, это сохранить не битовое представление float в 4 ubytes, а биты мантиссы (или представление с фиксированной точкой). Для этого вам нужно знать приблизительный диапазон поплавков (я предположу [0,1] здесь для простоты, иначе вам придется масштабировать по-другому, из конечно):

r = clamp(int(2^8 * f), 0, 255);
g = clamp(int(2^16 * f), 0, 255);
b = clamp(int(2^24 * f), 0, 255);     //only have 24 bits of precision anyway

конечно, вы также можете работать напрямую с битами мантиссы. И тогда в шейдере вы можете просто реконструировать его таким образом, используя тот факт, что компоненты vec4 все в [0,1]:

f = (v.r) + (v.g / 2^8) + (v.b / 2^16);

хотя я не уверен, что это приведет к точно такому же значению, полномочия двух должны немного помочь.