/* * Color picking * * (C) Schrodinger, Inc. */ #include "Picking.h" #include #include constexpr auto MAX_BITS = 8u; /** * Value to put in the "unused bits" part of a color channel. * May serve several purposes: * - validation (check bits) * - no-pick vs. through-pick * - rounding * * Disclaimer: I'm not sure if rounding is ever needed and if rounding and * validation should be mutually exclusive. */ constexpr unsigned make_check_value(unsigned bits) { return 0x80u >> bits; } /** * Return true if this RGBA color looks valid, and false if it should * be ignored. */ bool PickColorConverter::validateCheckBits(const channel_t* rgba) const { for (unsigned i = 0; i != 4; ++i) { assert(m_rgba_and_check_bits[i] >= m_rgba_bits[i]); // mask to stamp out the validation bits const channel_t check_mask = (0xFFu >> m_rgba_bits[i]) & // ~(0xFFu >> m_rgba_and_check_bits[i]); const channel_t check_value = make_check_value(m_rgba_bits[i]); if ((rgba[i] & check_mask) != (check_value & check_mask)) { // found antialiased bit (or cPickableNoPick/cPickableThrough which can // also be ignored) return false; } } return true; } /** * Set the number of bits per picking channel * @param rgba_bits Number of available bits per channel * @param max_check_bits Maximum number of bits (per channel) to use for * validation */ void PickColorConverter::setRgbaBits(const int* rgba_bits, int max_check_bits) { for (unsigned i = 0; i != 4; ++i) { m_rgba_bits[i] = std::min(MAX_BITS, rgba_bits[i]); // Use at most half of the pixels for validation int const check_bits = std::min(m_rgba_bits[i] / 2, max_check_bits); m_rgba_and_check_bits[i] = m_rgba_bits[i]; m_rgba_bits[i] = std::max(0, int(m_rgba_bits[i]) - check_bits); } // Use one bit to distinguish no-pick and through-pick. // This bit only has to reach the shader, it's not relevant for the // output buffer. This means it can be part of the check bits, but // doesn't have to be. It can also be beyond the color depth of the // output buffer (In theory, I have no suitable system to test this // assumption). m_rgba_bits[3] = std::min(MAX_BITS - 1, m_rgba_bits[3]); } /** * Get the picking index from color * @param rgba RGBA color * @return Picking index */ PickColorConverter::index_t PickColorConverter::indexFromColor( const channel_t* rgba) const { if (!validateCheckBits(rgba)) { return 0; } unsigned idx = 0; unsigned bits = 0; for (unsigned i = 0; i != 4; ++i) { idx |= (unsigned(rgba[i]) >> (MAX_BITS - m_rgba_bits[i])) << bits; bits += m_rgba_bits[i]; } return idx; } /** * Get color from picking index * @param[out] rgba RGBA color * @param idx Picking index */ void PickColorConverter::colorFromIndex(channel_t* rgba, index_t idx) const { unsigned bits = 0; for (unsigned i = 0; i != 4; ++i) { rgba[i] = ((idx >> bits) & 0xFFu) << (MAX_BITS - m_rgba_bits[i]); rgba[i] |= make_check_value(m_rgba_bits[i]); bits += m_rgba_bits[i]; } } /** * Get no-pick color * @param[out] rgba RGBA color */ void PickColorConverter::colorNoPick(channel_t* rgba) const { rgba[0] = 0; rgba[1] = 0; rgba[2] = 0; rgba[3] = make_check_value(m_rgba_bits[3]); assert(rgba[3] != 0); } /** * Get pick-through color * @param[out] rgba RGBA color * * Note: This only works if the shader discards full-transparent pixels (e.g. may not * work in immediate mode) */ void PickColorConverter::colorPickThrough(channel_t* rgba) const { rgba[0] = 0; rgba[1] = 0; rgba[2] = 0; rgba[3] = 0; } /** * Get the color for (context, index, bond, current picking pass). */ void PickColorManager::colorNext(unsigned char* color, const PickContext* context, unsigned int index, int bond) { if (bond == cPickableNoPick) { colorNoPick(color); return; } if (bond == cPickableThrough) { // TODO has no effect on immediate mode (fragment not discarded) colorPickThrough(color); return; } const Picking p_new = {{index, bond}, *context}; assert(m_count <= m_identifiers.size()); // compare with previous pick color, increment if different if (m_count == 0 || m_identifiers[m_count - 1] != p_new) { ++m_count; } unsigned j = m_count; if (m_pass > 0) { assert(m_count <= m_identifiers.size()); j >>= getTotalBits() * m_pass; } else if (m_count == m_identifiers.size() + 1) { m_identifiers.push_back(p_new); } // if this assertion fails, then invalidate() was not called, but should be assert(m_identifiers[m_count - 1] == p_new); colorFromIndex(color, j); } /** * Get identifier for the 1-based picking color index. * Returns nullptr if the index is out of bounds. */ const Picking* PickColorManager::getIdentifier(unsigned index) const { if (index > 0 && index <= m_identifiers.size()) { return m_identifiers.data() + index - 1; } return nullptr; }