Skip to content

Commit

Permalink
moderngl#475 support normalized integer texture formats
Browse files Browse the repository at this point in the history
  • Loading branch information
einarf committed Nov 7, 2021
1 parent 8b06d24 commit b887f8f
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 70 deletions.
81 changes: 81 additions & 0 deletions docs/topics/texture_formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,87 @@ In shaders the sampler type should be ``isampler2D``, ``isampler2DArray``
| i4 | 4 | GL_RGBA_INTEGER | GL_RGBA32I |
+----------+---------------+-----------------+-------------------+

Normalized Integer Textures
---------------------------

Normalized integers are integer texture, but texel reads in a shader
returns normalized values (``[0.0, 1.0]``). For example an unsigned 16
bit fragment with the value ``2**16-1`` will be read as ``1.0``.

Normalized integer textures should use the `sampler2D` sampler
type. Also note that there's no standard for normalized 32 bit
integer textures because a float32 doesn't have enough precision
to express a 32 bit integer as a number between 0.0 and 1.0.

Unsigned
~~~~~~~~

``nu1`` textures is really the same as an ``f1``. Each component
is a ``GL_UNSIGNED_BYTE``, but are read by the shader in normalized
form ``[0.0, 1.0]``.

+----------+---------------+-----------------+-------------------+
| **dtype**| *Components* | *Base Format* | *Internal Format* |
+==========+===============+=================+===================+
| nu1 | 1 | GL_RED | GL_R8 |
+----------+---------------+-----------------+-------------------+
| nu1 | 2 | GL_RG | GL_RG8 |
+----------+---------------+-----------------+-------------------+
| nu1 | 3 | GL_RGB | GL_RGB8 |
+----------+---------------+-----------------+-------------------+
| nu1 | 4 | GL_RGBA | GL_RGBA8 |
+----------+---------------+-----------------+-------------------+

``nu2`` textures store 16 bit unsigned integers (``GL_UNSIGNED_SHORT``).
The value range ``[0, 2**16-1]`` will be normalized into ``[0.0, 1.0]``.

+----------+---------------+-----------------+-------------------+
| **dtype**| *Components* | *Base Format* | *Internal Format* |
+==========+===============+=================+===================+
| nu2 | 1 | GL_RED | GL_R16 |
+----------+---------------+-----------------+-------------------+
| nu2 | 2 | GL_RG | GL_RG16 |
+----------+---------------+-----------------+-------------------+
| nu2 | 3 | GL_RGB | GL_RGB16 |
+----------+---------------+-----------------+-------------------+
| nu2 | 4 | GL_RGBA | GL_RGBA16 |
+----------+---------------+-----------------+-------------------+

Signed
~~~~~~

``ni1`` textures store 8 bit signed integers (``GL_BYTE``).
The value range ``[0, 127]`` will be normalized into ``[0.0, 1.0]``.
Negative values will be clamped.

+----------+---------------+-----------------+-------------------+
| **dtype**| *Components* | *Base Format* | *Internal Format* |
+==========+===============+=================+===================+
| ni1 | 1 | GL_RED | GL_R8 |
+----------+---------------+-----------------+-------------------+
| ni1 | 2 | GL_RG | GL_RG8 |
+----------+---------------+-----------------+-------------------+
| ni1 | 3 | GL_RGB | GL_RGB8 |
+----------+---------------+-----------------+-------------------+
| ni1 | 4 | GL_RGBA | GL_RGBA8 |
+----------+---------------+-----------------+-------------------+

``ni2`` textures store 16 bit signed integers (``GL_SHORT``).
The value range ``[0, 2**15-1]`` will be normalized into ``[0.0, 1.0]``.
Negative values will be clamped.

+----------+---------------+-----------------+-------------------+
| **dtype**| *Components* | *Base Format* | *Internal Format* |
+==========+===============+=================+===================+
| ni2 | 1 | GL_RED | GL_R16 |
+----------+---------------+-----------------+-------------------+
| ni2 | 2 | GL_RG | GL_RG16 |
+----------+---------------+-----------------+-------------------+
| ni2 | 3 | GL_RGB | GL_RGB16 |
+----------+---------------+-----------------+-------------------+
| ni2 | 4 | GL_RGBA | GL_RGBA16 |
+----------+---------------+-----------------+-------------------+

Overriding internalformat
-------------------------

Expand Down
82 changes: 57 additions & 25 deletions moderngl/src/DataType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ static int i1_internal_format[5] = {0, GL_R8I, GL_RG8I, GL_RGB8I, GL_RGBA8I};
static int i2_internal_format[5] = {0, GL_R16I, GL_RG16I, GL_RGB16I, GL_RGBA16I};
static int i4_internal_format[5] = {0, GL_R32I, GL_RG32I, GL_RGB32I, GL_RGBA32I};

static int n1_internal_format[5] = {0, GL_R8, GL_RG8, GL_RGB8, GL_RGBA8};
static int n2_internal_format[5] = {0, GL_R16, GL_RG16, GL_RGB16, GL_RGBA16};

static MGLDataType f1 = {float_base_format, f1_internal_format, GL_UNSIGNED_BYTE, 1, true};
static MGLDataType f2 = {float_base_format, f2_internal_format, GL_HALF_FLOAT, 2, true};
static MGLDataType f4 = {float_base_format, f4_internal_format, GL_FLOAT, 4, true};
Expand All @@ -23,40 +26,69 @@ static MGLDataType i1 = {int_base_format, i1_internal_format, GL_BYTE, 1, false}
static MGLDataType i2 = {int_base_format, i2_internal_format, GL_SHORT, 2, false};
static MGLDataType i4 = {int_base_format, i4_internal_format, GL_INT, 4, false};

MGLDataType * from_dtype(const char * dtype) {
if (!dtype[0] || (dtype[1] && dtype[2])) {
return 0;
}
static MGLDataType nu1 = {float_base_format, n1_internal_format, GL_UNSIGNED_BYTE, 1, false};
static MGLDataType nu2 = {float_base_format, n2_internal_format, GL_UNSIGNED_SHORT, 2, false};
static MGLDataType ni1 = {float_base_format, n1_internal_format, GL_BYTE, 1, false};
static MGLDataType ni2 = {float_base_format, n2_internal_format, GL_SHORT, 2, false};

MGLDataType * from_dtype(const char * dtype, Py_ssize_t size) {
if (size < 2 || size > 3) return 0;

// if (!dtype[0] || (dtype[1] && dtype[2])) {
// return 0;
// }

if (size == 2) {
switch (dtype[0] * 256 + dtype[1]) {
case ('f' * 256 + '1'):
return &f1;

switch (dtype[0] * 256 + dtype[1]) {
case ('f' * 256 + '1'):
return &f1;
case ('f' * 256 + '2'):
return &f2;

case ('f' * 256 + '2'):
return &f2;
case ('f' * 256 + '4'):
return &f4;

case ('f' * 256 + '4'):
return &f4;
case ('u' * 256 + '1'):
return &u1;

case ('u' * 256 + '1'):
return &u1;
case ('u' * 256 + '2'):
return &u2;

case ('u' * 256 + '2'):
return &u2;
case ('u' * 256 + '4'):
return &u4;

case ('u' * 256 + '4'):
return &u4;
case ('i' * 256 + '1'):
return &i1;

case ('i' * 256 + '2'):
return &i2;

case ('i' * 256 + '4'):
return &i4;

default:
return 0;
}
}
else if (size == 3)
{
switch (dtype[0] * 65536 + dtype[1] * 256 + dtype[2])
{
case ('n' * 65536 + 'i' * 256 + '1'):
return &ni1;

case ('i' * 256 + '1'):
return &i1;
case ('n' * 65536 + 'i' * 256 + '2'):
return &ni2;

case ('i' * 256 + '2'):
return &i2;
case ('n' * 65536 + 'u' * 256 + '1'):
return &nu1;

case ('i' * 256 + '4'):
return &i4;
case ('n' * 65536 + 'u' * 256 + '2'):
return &nu2;

default:
return 0;
default:
return 0;
}
}
}
14 changes: 2 additions & 12 deletions moderngl/src/Framebuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -530,12 +530,7 @@ PyObject * MGLFramebuffer_read(MGLFramebuffer * self, PyObject * args) {
return 0;
}

if (dtype_size != 2) {
MGLError_Set("invalid dtype");
return 0;
}

MGLDataType * data_type = from_dtype(dtype);
MGLDataType * data_type = from_dtype(dtype, dtype_size);

if (!data_type) {
MGLError_Set("invalid dtype");
Expand Down Expand Up @@ -651,12 +646,7 @@ PyObject * MGLFramebuffer_read_into(MGLFramebuffer * self, PyObject * args) {
return 0;
}

if (dtype_size != 2) {
MGLError_Set("invalid dtype");
return 0;
}

MGLDataType * data_type = from_dtype(dtype);
MGLDataType * data_type = from_dtype(dtype, dtype_size);

if (!data_type) {
MGLError_Set("invalid dtype");
Expand Down
9 changes: 2 additions & 7 deletions moderngl/src/Renderbuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,7 @@ PyObject * MGLContext_renderbuffer(MGLContext * self, PyObject * args) {
return 0;
}

if (dtype_size != 2) {
MGLError_Set("invalid dtype");
return 0;
}

MGLDataType * data_type = from_dtype(dtype);
MGLDataType * data_type = from_dtype(dtype, dtype_size);

if (!data_type) {
MGLError_Set("invalid dtype");
Expand Down Expand Up @@ -137,7 +132,7 @@ PyObject * MGLContext_depth_renderbuffer(MGLContext * self, PyObject * args) {
renderbuffer->height = height;
renderbuffer->components = 1;
renderbuffer->samples = samples;
renderbuffer->data_type = from_dtype("f4");
renderbuffer->data_type = from_dtype("f4", 2);
renderbuffer->depth = true;

Py_INCREF(self);
Expand Down
9 changes: 2 additions & 7 deletions moderngl/src/Texture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,7 @@ PyObject * MGLContext_texture(MGLContext * self, PyObject * args) {
return 0;
}

if (dtype_size != 2) {
MGLError_Set("invalid dtype");
return 0;
}

MGLDataType * data_type = from_dtype(dtype);
MGLDataType * data_type = from_dtype(dtype, dtype_size);

if (!data_type) {
MGLError_Set("invalid dtype");
Expand Down Expand Up @@ -259,7 +254,7 @@ PyObject * MGLContext_depth_texture(MGLContext * self, PyObject * args) {
texture->height = height;
texture->components = 1;
texture->samples = samples;
texture->data_type = from_dtype("f4");
texture->data_type = from_dtype("f4", 2);

texture->compare_func = GL_LEQUAL;
texture->depth = true;
Expand Down
7 changes: 1 addition & 6 deletions moderngl/src/Texture3D.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,7 @@ PyObject * MGLContext_texture3d(MGLContext * self, PyObject * args) {
return 0;
}

if (dtype_size != 2) {
MGLError_Set("invalid dtype");
return 0;
}

MGLDataType * data_type = from_dtype(dtype);
MGLDataType * data_type = from_dtype(dtype, dtype_size);

if (!data_type) {
MGLError_Set("invalid dtype");
Expand Down
7 changes: 1 addition & 6 deletions moderngl/src/TextureArray.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,7 @@ PyObject * MGLContext_texture_array(MGLContext * self, PyObject * args) {
return 0;
}

if (dtype_size != 2) {
MGLError_Set("invalid dtype");
return 0;
}

MGLDataType * data_type = from_dtype(dtype);
MGLDataType * data_type = from_dtype(dtype, dtype_size);

if (!data_type) {
MGLError_Set("invalid dtype");
Expand Down
7 changes: 1 addition & 6 deletions moderngl/src/TextureCube.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,7 @@ PyObject * MGLContext_texture_cube(MGLContext * self, PyObject * args) {
return 0;
}

if (dtype_size != 2) {
MGLError_Set("invalid dtype");
return 0;
}

MGLDataType * data_type = from_dtype(dtype);
MGLDataType * data_type = from_dtype(dtype, dtype_size);

if (!data_type) {
MGLError_Set("invalid dtype");
Expand Down
2 changes: 1 addition & 1 deletion moderngl/src/Types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ struct MGLSampler {
float max_lod;
};

MGLDataType * from_dtype(const char * dtype);
MGLDataType * from_dtype(const char * dtype, Py_ssize_t size);

void MGLAttribute_Invalidate(MGLAttribute * attribute);
void MGLBuffer_Invalidate(MGLBuffer * buffer);
Expand Down
47 changes: 47 additions & 0 deletions tests/test_texture.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import struct
import unittest
from array import array
import platform

import moderngl
Expand Down Expand Up @@ -161,6 +162,52 @@ def test_override_internalformat(self):
data = texture.read()
self.assertEqual(self.ctx.error, "GL_NO_ERROR")

def test_normalized_textures(self):
"""8 and 16 bit normalized integer textures"""
ni1 = self.ctx.texture((4, 4), 4, dtype="ni1")
ni2 = self.ctx.texture((4, 4), 4, dtype="ni2")
nu1 = self.ctx.texture((4, 4), 4, dtype="nu1")
nu2 = self.ctx.texture((4, 4), 4, dtype="nu2")

nu1_data = array('B', [255] * 16 * 4).tobytes()
nu2_data = array('H', [65535] * 16 * 4).tobytes()
ni1_data = array('b', [127] * 16 * 4).tobytes()
ni2_data = array('h', [32767] * 16 * 4).tobytes()

nu1.write(nu1_data)
nu2.write(nu2_data)
ni1.write(ni1_data)
ni2.write(ni2_data)

self.assertEqual(nu1.read(), nu1_data)
self.assertEqual(nu2.read(), nu2_data)
self.assertEqual(ni1.read(), ni1_data)
self.assertEqual(ni2.read(), ni2_data)

fbo = self.ctx.simple_framebuffer((4, 4))
fbo.use()

# Render these textures to an RGBA8 framebuffer and ensure the result is a pure white color (FF)
fbo.clear()
nu1.use()
self.vao.render()
self.assertEqual(fbo.read(viewport=(0, 0, 1, 1), components=4, dtype="f1"), b'\xff\xff\xff\xff')

fbo.clear()
nu2.use()
self.vao.render()
self.assertEqual(fbo.read(viewport=(0, 0, 1, 1), components=4, dtype="f1"), b'\xff\xff\xff\xff')

fbo.clear()
ni1.use()
self.vao.render()
self.assertEqual(fbo.read(viewport=(0, 0, 1, 1), components=4, dtype="f1"), b'\xff\xff\xff\xff')

fbo.clear()
ni2.use()
self.vao.render()
self.assertEqual(fbo.read(viewport=(0, 0, 1, 1), components=4, dtype="f1"), b'\xff\xff\xff\xff')


if __name__ == '__main__':
unittest.main()

0 comments on commit b887f8f

Please sign in to comment.