Replies: 6 comments 14 replies
-
I'm on board with these.
Color interpolation is 100% the most important math operation.
I don't like this much.
I strongly dislike this.
This is unsurprisingly the sacrifice I'd personally be most willing to make, but I think it's the wrong choice.
Yeah, no strong feelings here. Either would be fine; the important thing is that the data representation is correct when at rest. I suspect that doing the conversion immediately will result in nicer properties, since presumably the target space is the one whose metric you care about.
Yes please. We should have optional features for conversions into all of the big color crates. I don't want end users to end up having to newtype their colors to get conversion. |
Beta Was this translation helpful? Give feedback.
-
Do we want to keep an internal representation of the color close to what the gpu is expecting? If I set up a color as HSL, as the gpu is expecting a color in RGB the conversion will happen on every frame. If we keep the converted value inside the color type, and update it only when the color change in HSL, we can reduce the number of times we do the conversion. |
Beta Was this translation helpful? Give feedback.
-
The Discord discussion about operations on colours was color.op(other, operation_space) -> operation_space, if I understood correctly. Both color and other would be converted to the operation_space, and the operation would be applied in that space and the output produced. Your suggestion differs from this. Why? |
Beta Was this translation helpful? Give feedback.
-
Could it make sense to have a enum AND colorspace types? For example: pub enum Color {
Srgba(Srgba),
SrgbaLinear(SrgbaLinear),
HSL(HSL),
...
}
pub struct Srgba(Vec4);
impl From<Srgba> for Color {
...
}
impl From<Color> to Srgba {
...
} This adds notable complexity internally to the color API, but provides the same level of flexibility in userspace and stringent consistency where we don't need that flexibility (i.e. GPU submit, internal animation operations, etc). We can still have nice user-facing shorthands like |
Beta Was this translation helpful? Give feedback.
-
I started another ticket about color APIs: #10986 . Apologies for the duplication; this was a spin-off from the I've put together a small prototype library here: https://github.com/viridia/quill/blob/main/crates/bevy_color/src/lib.rs Key aspects of this library:
I considered adding additional traits such as For the rare case where you have the wrong encoding (your image asset reader gives you SRGB types, but the actual image data is linear) there's a way to transmute without converting: Note that the JavaScript 3D framework, three.js, makes it the user's responsibility to convert colors into the correct color space before calling framework APIs. Unfortunately, their solution is not type-safe, making it somewhat of a footgun; with Rust we can do better by ensuring the user does the conversion to the correct type. Still, it provides support to the idea that the right long-term solution for users is not to hide color spaces from them, but to educate them in their proper usage. Otherwise we're just sweeping problems under the rug. |
Beta Was this translation helpful? Give feedback.
-
My prototype library can now be found here: https://github.com/viridia/bevy_color |
Beta Was this translation helpful? Give feedback.
-
As the color conversation is "hot" again and animation apis are starting to move forward, it makes sense to take a final pass over the
Color
api.Please note that this initial message is meant to kick off a conversation, not dictate a final design.
Ive shown that (1) is in fact a "problem" here by adding a
palette
adapter for sprite colors (with linear srgb as the "base type" and HSL as the type converted to/from each frame). However after a few frames of precision loss the color stabilizes on a final value. The precision loss is "small" (at least in this specific case) and in practice seems like it won't manifest visibly in a meaningful way. I'd want to prove that globally before assuming thats true though. What it does absolutely prevent is things likeif color == Color::RED
because setting a color to red would immediately "drift" to something "approximately full red". Clippy would yell at you for float comparison ... but in practice i think this type of comparison in this context is both fine and useful. Floats are stable for specific values ... it's operations that introduce problems.I personally don't think we need one. The conversion algorithms are well documented / straightforward and none of the implementations I'm aware of meet all of the requirements above. The options I'm aware of fall into one or more of these categories:
I'm mostly against (2)-style apis for aesthetic reasons. For a "public" monolithic color api, it feels wrong (and confusing) to store the H component in HSL and the R component in RGB inside the same
vec3.x
field. Especially when enums can give complete type safety and clarity without munging things together. There are valid reasons to use an untyped vec (mathy reasons like "color space conversions can be represented as matrix transforms" and performance reasons like "we can more easily use simd ops"), but none of those are worth the clarity cost to me or feel particularly relevant to our public color api. With enum representations we can use Rust features like pattern matching (if let Color::rgb { r, .. } = some_color
) and users looking at the type can immediately understand how it works in a clear, magic-free way.Theres also the concern that we can't easily tweak types in upstream crates. Needing to bug maintainers to add support for traits like Reflect or Component isn't fun for anyone.
I'm very open to adding opt-in support for conversion to/from external library colors to our Color type, but I don't think we should be adopting one of their types as our user-facing Color type.
Color::rgb(1.0, 0.5, 0.2)
,Color::hsl(0.5, 0.8, 0.2)
,Color::rgb_linear(1.0, 1.0, 1.0)
. Internally these are represented as an enum (ex:Color::Rgb { r: 1.0, g: 0.5, b: 0.2 }
).some_color.add(Color::RED, ColorSpace::Rgb)
some_rgb_color.lerp(Color::Hsl {...}, 0.5)
would default to HSL (the "right hand side") because the color should end on the exact Hsl value when it hits 1.0. Whether position 0.0 in the interpolation should be RGB (left hand side) or HSL (right hand side) is an arbitrary choice we'd need to make. I'm personally biased toward setting it to RGB to maintain the "integrity" of the original position, but in practice I don't think this will matter much. The "flip" approach feels consistent with animating types like bools, which will need to "flip" their state at some point (ex: godot supports animating bools in their high level system, which sounds weird but is very useful in practice).In practice, this is largely how things work already. The biggest change would be deprecating operations that do implicit color conversion (ex: multiplying and adding two Colors) and replacing them with more explicit apis.
I personally don't think so unless there is significant pushback here. I think we're very close to having our ideal api. But feel free to voice concerns.
Beta Was this translation helpful? Give feedback.
All reactions