Как преобразовать значение vec4 rgba в число с плавающей точкой?

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

5 ответов

Решение

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

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

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

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

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, которые я нашел полезными. Ссылка1. Ссылка2. Ссылка3.

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

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

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

Я узнал что:

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

Используя эти небольшие модификации, я получил одинаковый вывод почти на всех входах и получил только небольшие ошибки между исходным и закодированным / декодированным значением, когда что-то пошло не так, в то время как в оригинальной реализации ошибок округления 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-битный float, а знак упакован с мантиссой:

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

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

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

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

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

Таким образом, если ваш входной массив имеет значение 183,45, значение в Uint8Array будет 183. Значение в текстуре будет 183/255 или 0,718. Если ваше входное значение было 0,45, Uint8Array будет содержать 0, а результат текстуры будет 0,0.

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

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

Кстати, Khronos действительно должно быть стыдно за то, что даже назвал WebGL спецификацией. Это едва определяет что-либо; это просто набор ссылок на другие спецификации, что делает поиск эффектов чего-либо чрезвычайно сложным.

Вы не сможете просто интерпретировать 4 неподписанных байта как биты значения с плавающей запятой (что, я полагаю, вам нужно) в шейдере (по крайней мере, не в GLES или WebGL, я думаю). То, что вы можете сделать, это не сохранить битовое представление с плавающей запятой в 4-х байтах, а биты мантиссы (или представление с фиксированной точкой). Для этого вам нужно знать приблизительный диапазон значений с плавающей точкой (для простоты я приму здесь [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);

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

Другие вопросы по тегам