Files
pymol-open-source/layer1/Picking.cpp
2024-05-20 09:07:33 -04:00

199 lines
4.9 KiB
C++

/*
* Color picking
*
* (C) Schrodinger, Inc.
*/
#include "Picking.h"
#include <algorithm>
#include <cassert>
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<unsigned>(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<unsigned>(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;
}