From 9dcfcbf5fae9d29796da91be62fef552d22956d4 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 19 Dec 2017 20:02:10 +0100 Subject: [PATCH] texture: support store() on more component types (incl. sRGB) This changes behaviour for sRGB textures, which weren't previously converting to the correct color space. Also add unit tests for storing to PNMImage. Closes: #212 --- panda/src/gobj/texture.cxx | 116 +++++++++++++++++++++++++--------- panda/src/gobj/texture.h | 3 +- panda/src/pnmimage/pnmImage.I | 2 +- tests/gobj/test_texture.py | 90 ++++++++++++++++++++++++++ 4 files changed, 179 insertions(+), 32 deletions(-) create mode 100644 tests/gobj/test_texture.py diff --git a/panda/src/gobj/texture.cxx b/panda/src/gobj/texture.cxx index c65172364e..c4a8149ab4 100644 --- a/panda/src/gobj/texture.cxx +++ b/panda/src/gobj/texture.cxx @@ -5088,7 +5088,8 @@ do_store_one(CData *cdata, PNMImage &pnmimage, int z, int n) { return convert_to_pnmimage(pnmimage, do_get_expected_mipmap_x_size(cdata, n), do_get_expected_mipmap_y_size(cdata, n), - cdata->_num_components, cdata->_component_width, + cdata->_num_components, cdata->_component_type, + is_srgb(cdata->_format), cdata->_ram_images[n]._image, do_get_ram_mipmap_page_size(cdata, n), z); } @@ -5111,12 +5112,14 @@ do_store_one(CData *cdata, PfmFile &pfm, int z, int n) { if (cdata->_component_type != T_float) { // PfmFile by way of PNMImage. PNMImage pnmimage; - bool success = convert_to_pnmimage(pnmimage, - do_get_expected_mipmap_x_size(cdata, n), - do_get_expected_mipmap_y_size(cdata, n), - cdata->_num_components, cdata->_component_width, - cdata->_ram_images[n]._image, - do_get_ram_mipmap_page_size(cdata, n), z); + bool success = + convert_to_pnmimage(pnmimage, + do_get_expected_mipmap_x_size(cdata, n), + do_get_expected_mipmap_y_size(cdata, n), + cdata->_num_components, cdata->_component_type, + is_srgb(cdata->_format), + cdata->_ram_images[n]._image, + do_get_ram_mipmap_page_size(cdata, n), z); if (!success) { return false; } @@ -8055,25 +8058,28 @@ convert_from_pfm(PTA_uchar &image, size_t page_size, int z, */ bool Texture:: convert_to_pnmimage(PNMImage &pnmimage, int x_size, int y_size, - int num_components, int component_width, - CPTA_uchar image, size_t page_size, int z) { + int num_components, ComponentType component_type, + bool is_srgb, CPTA_uchar image, size_t page_size, int z) { xelval maxval = 0xff; - if (component_width > 1) { + if (component_type != T_unsigned_byte && component_type != T_byte) { maxval = 0xffff; } - pnmimage.clear(x_size, y_size, num_components, maxval); + ColorSpace color_space = is_srgb ? CS_sRGB : CS_linear; + pnmimage.clear(x_size, y_size, num_components, maxval, nullptr, color_space); bool has_alpha = pnmimage.has_alpha(); bool is_grayscale = pnmimage.is_grayscale(); int idx = page_size * z; nassertr(idx + page_size <= image.size(), false); - const unsigned char *p = &image[idx]; - if (component_width == 1) { - xel *array = pnmimage.get_array(); + xel *array = pnmimage.get_array(); + xelval *alpha = pnmimage.get_alpha_array(); + + switch (component_type) { + case T_unsigned_byte: if (is_grayscale) { + const unsigned char *p = &image[idx]; if (has_alpha) { - xelval *alpha = pnmimage.get_alpha_array(); for (int j = y_size-1; j >= 0; j--) { xel *row = array + j * x_size; xelval *alpha_row = alpha + j * x_size; @@ -8090,9 +8096,10 @@ convert_to_pnmimage(PNMImage &pnmimage, int x_size, int y_size, } } } + nassertr(p == &image[idx] + page_size, false); } else { + const unsigned char *p = &image[idx]; if (has_alpha) { - xelval *alpha = pnmimage.get_alpha_array(); for (int j = y_size-1; j >= 0; j--) { xel *row = array + j * x_size; xelval *alpha_row = alpha + j * x_size; @@ -8113,29 +8120,78 @@ convert_to_pnmimage(PNMImage &pnmimage, int x_size, int y_size, } } } + nassertr(p == &image[idx] + page_size, false); } + break; - } else if (component_width == 2) { - for (int j = y_size-1; j >= 0; j--) { - for (int i = 0; i < x_size; i++) { - if (is_grayscale) { - pnmimage.set_gray(i, j, get_unsigned_short(p)); - } else { - pnmimage.set_blue(i, j, get_unsigned_short(p)); - pnmimage.set_green(i, j, get_unsigned_short(p)); - pnmimage.set_red(i, j, get_unsigned_short(p)); - } - if (has_alpha) { - pnmimage.set_alpha(i, j, get_unsigned_short(p)); + case T_unsigned_short: + { + const uint16_t *p = (const uint16_t *)&image[idx]; + + for (int j = y_size-1; j >= 0; j--) { + xel *row = array + j * x_size; + xelval *alpha_row = alpha + j * x_size; + for (int i = 0; i < x_size; i++) { + PPM_PUTB(row[i], *p++); + if (!is_grayscale) { + PPM_PUTG(row[i], *p++); + PPM_PUTR(row[i], *p++); + } + if (has_alpha) { + alpha_row[i] = *p++; + } } } + nassertr((const unsigned char *)p == &image[idx] + page_size, false); } + break; - } else { + case T_unsigned_int: + { + const uint32_t *p = (const uint32_t *)&image[idx]; + + for (int j = y_size-1; j >= 0; j--) { + xel *row = array + j * x_size; + xelval *alpha_row = alpha + j * x_size; + for (int i = 0; i < x_size; i++) { + PPM_PUTB(row[i], (*p++) >> 16u); + if (!is_grayscale) { + PPM_PUTG(row[i], (*p++) >> 16u); + PPM_PUTR(row[i], (*p++) >> 16u); + } + if (has_alpha) { + alpha_row[i] = (*p++) >> 16u; + } + } + } + nassertr((const unsigned char *)p == &image[idx] + page_size, false); + } + break; + + case T_half_float: + { + const unsigned char *p = &image[idx]; + + for (int j = y_size-1; j >= 0; j--) { + for (int i = 0; i < x_size; i++) { + pnmimage.set_blue(i, j, get_half_float(p)); + if (!is_grayscale) { + pnmimage.set_green(i, j, get_half_float(p)); + pnmimage.set_red(i, j, get_half_float(p)); + } + if (has_alpha) { + pnmimage.set_alpha(i, j, get_half_float(p)); + } + } + } + nassertr(p == &image[idx] + page_size, false); + } + break; + + default: return false; } - nassertr(p == &image[idx] + page_size, false); return true; } diff --git a/panda/src/gobj/texture.h b/panda/src/gobj/texture.h index 49e2f48dbd..5816b8c538 100644 --- a/panda/src/gobj/texture.h +++ b/panda/src/gobj/texture.h @@ -801,7 +801,8 @@ private: int z, const PfmFile &pfm, int num_components, int component_width); static bool convert_to_pnmimage(PNMImage &pnmimage, int x_size, int y_size, - int num_components, int component_width, + int num_components, + ComponentType component_type, bool is_srgb, CPTA_uchar image, size_t page_size, int z); static bool convert_to_pfm(PfmFile &pfm, int x_size, int y_size, diff --git a/panda/src/pnmimage/pnmImage.I b/panda/src/pnmimage/pnmImage.I index 173f543e1e..d4a7026483 100644 --- a/panda/src/pnmimage/pnmImage.I +++ b/panda/src/pnmimage/pnmImage.I @@ -80,7 +80,7 @@ to_val(float input_value) const { switch (_xel_encoding) { case XE_generic: case XE_generic_alpha: - return clamp_val((int)(input_value * get_maxval() + 0.5f)); + return (int)(min(1.0f, max(0.0f, input_value)) * get_maxval() + 0.5f); case XE_generic_sRGB: case XE_generic_sRGB_alpha: diff --git a/tests/gobj/test_texture.py b/tests/gobj/test_texture.py new file mode 100644 index 0000000000..8fa0ed27c8 --- /dev/null +++ b/tests/gobj/test_texture.py @@ -0,0 +1,90 @@ +from panda3d.core import Texture, PNMImage +from array import array + + +def image_from_stored_pixel(component_type, format, data): + """ Creates a 1-pixel texture with the given settings and pixel data, + then returns a PNMImage as result of calling texture.store(). """ + + tex = Texture("") + tex.setup_1d_texture(1, component_type, format) + tex.set_ram_image(data) + + img = PNMImage() + assert tex.store(img) + return img + + +def test_texture_store_unsigned_byte(): + data = array('B', (2, 1, 0, 0xff)) + img = image_from_stored_pixel(Texture.T_unsigned_byte, Texture.F_rgba, data) + + assert img.maxval == 0xff + pix = img.get_pixel(0, 0) + assert tuple(pix) == (0, 1, 2, 0xff) + + +def test_texture_store_unsigned_short(): + data = array('H', (2, 1, 0, 0xffff)) + img = image_from_stored_pixel(Texture.T_unsigned_short, Texture.F_rgba, data) + + assert img.maxval == 0xffff + pix = img.get_pixel(0, 0) + assert tuple(pix) == (0, 1, 2, 0xffff) + + +def test_texture_store_unsigned_int(): + data = array('I', (0x30000, 2, 0, 0xffffffff)) + img = image_from_stored_pixel(Texture.T_unsigned_int, Texture.F_rgba, data) + + assert img.maxval == 0xffff + pix = img.get_pixel(0, 0) + assert tuple(pix) == (0, 0, 3, 0xffff) + + +def test_texture_store_float(): + data = array('f', (0.5, 0.0, -2.0, 10000.0)) + img = image_from_stored_pixel(Texture.T_float, Texture.F_rgba, data) + + assert img.maxval == 0xffff + col = img.get_xel_a(0, 0) + assert col.almost_equal((0.0, 0.0, 0.5, 1.0), 1 / 255.0) + + +def test_texture_store_half(): + # Python's array class doesn't support half floats, so we hardcode the + # binary representation of these numbers: + data = array('H', ( + # -[exp][mantissa] + 0b0011110000000000, # 1.0 + 0b1100000000000000, # -2.0 + 0b0111101111111111, # 65504.0 + 0b0011010101010101, # 0.333251953125 + )) + img = image_from_stored_pixel(Texture.T_half_float, Texture.F_rgba, data) + + assert img.maxval == 0xffff + col = img.get_xel_a(0, 0) + assert col.almost_equal((1.0, 0.0, 1.0, 0.333251953125), 1 / 255.0) + + +def test_texture_store_srgb(): + # 188 = roughly middle gray + data = array('B', [188, 188, 188]) + img = image_from_stored_pixel(Texture.T_unsigned_byte, Texture.F_srgb, data) + + # We allow some imprecision. + assert img.maxval == 0xff + col = img.get_xel(0, 0) + assert col.almost_equal((0.5, 0.5, 0.5), 1 / 255.0) + + +def test_texture_store_srgb_alpha(): + # 188 = middle gray + data = array('B', [188, 188, 188, 188]) + img = image_from_stored_pixel(Texture.T_unsigned_byte, Texture.F_srgb_alpha, data) + + # We allow some imprecision. + assert img.maxval == 0xff + col = img.get_xel_a(0, 0) + assert col.almost_equal((0.5, 0.5, 0.5, 188 / 255.0), 1 / 255.0)