-
Notifications
You must be signed in to change notification settings - Fork 60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a Spectrum type #70
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for picking this up! I'm not sure why I stopped responding to the other PR, but it seem to coincide with me moving to a new apartment and having a lot to do at work. (Sorry @tatref) It was a period where I had too much to hold in my head at the same time. 😞
Either way, I think this can be a good addition and that it should be as integrated as possible. That means that I think it should be considered a color format and be able to be converted to other color formats, in addition to xyz. (See inline comment)
I would also like to bring up the semantics of adding, subtracting, dividing and multiplying spectra with each other. What does one expect from one_red / another_red + some_green
? The same as in RGB? If that's the case and we implement those operators for RGB, then go for it!
Oh, and an operation that we could add is to be able to add (and maybe subtract, multiply and divide) spikes/pulses. Like a single wavelength. Either that or some other smart way to do single sample operations, just like you can manipulate the components of the other color formats.
As for the name of from_sparse
, I'm not a fan either. Maybe something like from_samples
or from_interpolated
? Naming is not my main strength...
src/spectrum.rs
Outdated
pub const SPECTRUM_MAX_LAMBDA: f32 = 830.0; | ||
|
||
pub const SPECTRUM_SAMPLES: usize = 95; | ||
const SPECTRUM_SAMPLE_STEP: usize = 5; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where are these numbers derived from? Can if they are related to each other, can they be expressed as a constant expression?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've refactored the constants to use constant expressions and removed the redundant 'SPECTRUM' naming (as this is obvious given we're in the 'spectrum.rs' module).
src/spectrum.rs
Outdated
let mut data: [T; SPECTRUM_SAMPLES] = [T::zero(); SPECTRUM_SAMPLES]; | ||
for (sample, cloned) in self.data.iter().zip(data.iter_mut()) { | ||
*cloned = *sample; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it's practically the same, but perhaps clone_from_slice
could be used here. It could at least help readability a bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool! Didn't know about that method, though it seems there's also copy_from_slice which it suggests might be faster so I've used that.
src/spectrum.rs
Outdated
/// values assumed to be in the range 360 nm to 830 nm. | ||
pub fn new(data: [T; SPECTRUM_SAMPLES]) -> Result<Self> { | ||
if data.iter().any(|&intensity| intensity < T::zero()) { | ||
Err(ErrorKind::SpectrumIntensityOutOfRange.into()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you consider using an assert!
or a debug assertion here instead? The question is who is responsible for checking the input? The library of the user? I don't remember if there are any other occurrences of this, but if we decide to keep it like this, then we could perhaps add _uchecked
versions of the constructors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess if we were going with what the rest of the library does then we should use assert!
and expect the caller to make sure there are no invalid values in the array.
I've updated the code to do this and that has removed the need for error-chain
and the errors.rs
module which I've also removed.
src/spectrum.rs
Outdated
/// The more data points there are the more accurate the | ||
/// `Spectrum`'s internal representation will be. | ||
pub fn from_sparse(data: &[(f32, T)]) -> Result<Self> { | ||
// TODO: replace this with sort_unstable_by() when stabilised. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rather have this todo in an issue when this is done. It won't be fixed anytime soon if it's hidden in the code.
src/spectrum.rs
Outdated
/// | ||
/// The more data points there are the more accurate the | ||
/// `Spectrum`'s internal representation will be. | ||
pub fn from_sparse(data: &[(f32, T)]) -> Result<Self> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks a lot like what Gradient
is doing. Could it be used internally and/or be taken as input somehow?
Also, is the step size small enough for supersampling to be unnecessary? I seem to remember that I used the area of the wavelength bin as a weight in my own path tracer toy, but I sampled multiple wavelengths from the start, instead of RGB. May be worth investigating, though, since it may give a better representation of spikes.
src/spectrum.rs
Outdated
} | ||
|
||
/// Converts a `Spectrum` to `Xyz` tristimulis values. | ||
pub fn to_xyz<Wp: WhitePoint<T>>(&self) -> Xyz<Wp, T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have been away for some time, so correct me if I'm not remembering everything, but there is a generic color conversion trait in here, somewhere. I think Spectrum
could implement that. It should have a bunch of semi automatic implementations to other formats, as well, so it should become fully compatible with other parts of the library. You may have to add some macro invocations somewhere for that to work 100%.
…edundancy in the naming
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for my delayed response. I'm visiting family, so my online presence is a bit spotty. Anyway, the changes looks good 👍, but there are some missing documentation and some naming that I think can be improved.
src/spectrum.rs
Outdated
/// The smallest wavelength represented in the `Spectrum` | ||
/// data structure. This is the first value in the | ||
/// map's wavelength. | ||
pub const MIN_LAMBDA: usize = 360; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking that perhaps we should name these {MIN, MAX}_WAVELENGTH
, since that's what they are, instead of which letter is used to represent them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had the exact same thought. Will push this change in the next commit, though I was thinking we might also want to include the units? (e.g. MAX_WAVELENGTH_NM).
src/spectrum.rs
Outdated
|
||
const SAMPLE_STEP: usize = 5; | ||
|
||
pub const N_SAMPLES: usize = (MAX_LAMBDA - MIN_LAMBDA) / SAMPLE_STEP + 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's something with the N_
prefix... Perhaps just SAMPLES
is good enough? Or is that ambiguous? SECTRUM_LENGTH
? SECTRUM_LEN
, to follow the .len()
pattern?
src/spectrum.rs
Outdated
/// The intensity values must be greater than or equal to | ||
/// zero and not NaN. | ||
/// | ||
pub fn from_samples(data: &[Sample<T>]) -> Spectrum<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could take &[S]
where S: Into<Sample<T>>
. And Sample<T>
could implement From<(f32, T)>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I'm still learning about all the power Rust gives you for creating convenient APIs.
Another design question I had was about whether we should allow the consumer of the API the ability to choose how many samples are stored in a Spectrum. Currently this is hard coded as 95 samples, but comparing this to pbrt for example. That only uses 30 samples, presumably because this is a happy medium between accuracy and speed but when compiling that you can just set that number to be as high or low as necessary. Obviously that wouldn't work for a library like It would also affect the performance of the various arithmetic operators as you would have to convert the two Perhaps we should consider dropping the number of samples from 95 to some lower number for the initial release of this feature to avoid the overhead of extra accuracy that the majority of users might not need? In addition, exposing a constructor (i.e. Sorry that was a bit of a mind dump :) |
I think it's fine to skip the unit. It's common to use nanometers and it can be clarified in the documentation. As for the resolution of the spectrum, I think it's a nice idea, but the problems with combining spectra makes it less worth it. It could work if/when Rust gets type level integers. Until then we could use empty types to define different sizes (see how color spaces works), but that's not as flexible. Do you have a working project that uses this? Would you like to do a bit of research and see if >30 makes a lot of difference? If it is a happy medium, then perhaps that's a good starting point. One thing I'm not sure about is how it works with conversion to and from XYZ. I guess each resolution would need its own curve data. |
Just an update to say I'm still working on this. Currently working on the finishing touches to a supersampling method for sampling the input data as mentioned in a previous review comment. |
Nice! There are still a couple of undocumented items, but it looks like I haven't added any automatic checks for that yet. Something that would be a nice addition is a small example in the examples directory. Maybe just a simple rainbow, or something. Otherwise I think it's a good first version of a spectrum type. |
I have added a new merge bot that replaces Homu, so I think it's best if you rebase your branch to the latest |
@acolley I guess this one was forgotten. Are you still interested? |
I'll close this due to inactivity, but feel free to pick it up again and continue if you ever want to. |
This is a PR for issue #64.
I had a need for a Spectrum class for a Ray Tracer project I'm working on and I noticed the existing PR had gone stale about a year ago. I didn't know if there was a way to edit that existing PR so I made this one.
I've based most of it off of the PR by tatref: #65. In addition I added the functions mentioned in the comments on that PR including the generic function for the mapping LUT and what I assumed was the 'gradient' method, which creates a sampling of a given input mapping wavelength to intensity using linear interpolation.
I've also added error handling using the error-chain library.
This adds two new dependencies to the library: ordered-float and error-chain.
One thing I wasn't sure of: what should the name of the 'gradient' method be? Currently it's called 'from_sparse' but I don't think that really reflects what it's doing.
The testing is also not perfect and I couldn't think of a great way of testing this sort of thing except some simple cases.