diff --git a/psychoblend/__init__.py b/psychoblend/__init__.py index f182333..ab718f2 100644 --- a/psychoblend/__init__.py +++ b/psychoblend/__init__.py @@ -81,13 +81,17 @@ class PsychopathCamera(bpy.types.PropertyGroup): class PsychopathLight(bpy.types.PropertyGroup): color_type = EnumProperty( name="Color Type", description="", - items=[('Rec709', 'Rec709', ""), ('Blackbody', 'Blackbody', "")], + items=[ + ('Rec709', 'Rec709', ""), + ('Blackbody', 'Blackbody', ""), + ('ColorTemperature', 'ColorTemperature', "Same as Blackbody, except with brightness kept more even."), + ], default="Rec709" ) color_blackbody_temp = FloatProperty( name="Temperature", description="Blackbody temperature in kelvin", - min=0.0, max=32000.0, soft_min=800.0, soft_max=6500.0, default=1200.0 + min=0.0, soft_min=800.0, soft_max=6500.0, default=1200.0 ) # Custom Mesh properties @@ -107,7 +111,11 @@ class PsychopathMaterial(bpy.types.PropertyGroup): color_type = EnumProperty( name="Color Type", description="", - items=[('Rec709', 'Rec709', ""), ('Blackbody', 'Blackbody', "")], + items=[ + ('Rec709', 'Rec709', ""), + ('Blackbody', 'Blackbody', ""), + ('ColorTemperature', 'ColorTemperature', "Same as Blackbody, except with brightness kept more even."), + ], default="Rec709" ) @@ -120,7 +128,7 @@ class PsychopathMaterial(bpy.types.PropertyGroup): color_blackbody_temp = FloatProperty( name="Temperature", description="Blackbody temperature in kelvin", - min=0.0, max=32000.0, soft_min=800.0, soft_max=6500.0, default=1200.0 + min=0.0, soft_min=800.0, soft_max=6500.0, default=1200.0 ) roughness = FloatProperty( diff --git a/psychoblend/assembly.py b/psychoblend/assembly.py index 8fbd442..aecb954 100644 --- a/psychoblend/assembly.py +++ b/psychoblend/assembly.py @@ -218,6 +218,8 @@ class SphereLamp: self.time_col += [('Rec709', self.ob.data.color * self.ob.data.energy)] elif self.ob.data.psychopath.color_type == 'Blackbody': self.time_col += [('Blackbody', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] + elif self.ob.data.psychopath.color_type == 'ColorTemperature': + self.time_col += [('ColorTemperature', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] self.time_rad += [self.ob.data.shadow_soft_size] @@ -234,6 +236,8 @@ class SphereLamp: w.write("Color [rec709, %f %f %f]\n" % (col[1][0], col[1][1], col[1][2])) elif col[0] == 'Blackbody': w.write("Color [blackbody, %f %f]\n" % (col[1], col[2])) + elif col[0] == 'ColorTemperature': + w.write("Color [color_temperature, %f %f]\n" % (col[1], col[2])) for rad in self.time_rad: w.write("Radius [%f]\n" % rad) @@ -257,6 +261,8 @@ class RectLamp: self.time_col += [('Rec709', self.ob.data.color * self.ob.data.energy)] elif self.ob.data.psychopath.color_type == 'Blackbody': self.time_col += [('Blackbody', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] + elif self.ob.data.psychopath.color_type == 'ColorTemperature': + self.time_col += [('ColorTemperature', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] if self.ob.data.shape == 'RECTANGLE': self.time_dim += [(self.ob.data.size, self.ob.data.size_y)] @@ -276,6 +282,8 @@ class RectLamp: w.write("Color [rec709, %f %f %f]\n" % (col[1][0], col[1][1], col[1][2])) elif col[0] == 'Blackbody': w.write("Color [blackbody, %f %f]\n" % (col[1], col[2])) + elif col[0] == 'ColorTemperature': + w.write("Color [color_temperature, %f %f]\n" % (col[1], col[2])) for dim in self.time_dim: w.write("Dimensions [%f %f]\n" % dim) @@ -340,6 +348,11 @@ class Material: self.mat.psychopath.color_blackbody_temp, 1.0, )) + elif self.mat.psychopath.color_type == 'ColorTemperature': + w.write("Color [color_temperature, %f %f]\n" % ( + self.mat.psychopath.color_blackbody_temp, + 1.0, + )) elif self.mat.psychopath.surface_shader_type == 'Lambert': w.write("Type [Lambert]\n") if self.mat.psychopath.color_type == 'Rec709': @@ -352,6 +365,11 @@ class Material: self.mat.psychopath.color_blackbody_temp, 1.0, )) + elif self.mat.psychopath.color_type == 'ColorTemperature': + w.write("Color [color_temperature, %f %f]\n" % ( + self.mat.psychopath.color_blackbody_temp, + 1.0, + )) elif self.mat.psychopath.surface_shader_type == 'GGX': w.write("Type [GGX]\n") if self.mat.psychopath.color_type == 'Rec709': @@ -364,6 +382,11 @@ class Material: self.mat.psychopath.color_blackbody_temp, 1.0, )) + elif self.mat.psychopath.color_type == 'ColorTemperature': + w.write("Color [color_temperature, %f %f]\n" % ( + self.mat.psychopath.color_blackbody_temp, + 1.0, + )) w.write("Roughness [%f]\n" % self.mat.psychopath.roughness) w.write("Fresnel [%f]\n" % self.mat.psychopath.fresnel) else: diff --git a/psychoblend/ui.py b/psychoblend/ui.py index 9f54115..6ebae8b 100644 --- a/psychoblend/ui.py +++ b/psychoblend/ui.py @@ -130,7 +130,7 @@ class DATA_PT_psychopath_lamp(PsychopathPanel, bpy.types.Panel): if ob.data.psychopath.color_type == 'Rec709': col.prop(ob.data, "color") - elif ob.data.psychopath.color_type == 'Blackbody': + elif ob.data.psychopath.color_type == 'Blackbody' or ob.data.psychopath.color_type == 'ColorTemperature': col.prop(ob.data.psychopath, "color_blackbody_temp") col.prop(ob.data, "energy") @@ -254,7 +254,7 @@ class MATERIAL_PT_psychopath_surface(PsychopathPanel, bpy.types.Panel): col.prop(mat.psychopath, "color_type") if mat.psychopath.color_type == 'Rec709': col.prop(mat.psychopath, "color") - elif mat.psychopath.color_type == 'Blackbody': + elif mat.psychopath.color_type == 'Blackbody' or mat.psychopath.color_type == 'ColorTemperature': col.prop(mat.psychopath, "color_blackbody_temp") if mat.psychopath.surface_shader_type == 'GTR': diff --git a/psychoblend/world.py b/psychoblend/world.py index 6620f48..c886fa4 100644 --- a/psychoblend/world.py +++ b/psychoblend/world.py @@ -137,6 +137,8 @@ class DistantDiskLamp: self.time_col += [('Rec709', self.ob.data.color * self.ob.data.energy)] elif self.ob.data.psychopath.color_type == 'Blackbody': self.time_col += [('Blackbody', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] + elif self.ob.data.psychopath.color_type == 'ColorTemperature': + self.time_col += [('ColorTemperature', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] self.time_rad += [self.ob.data.shadow_soft_size] @@ -151,6 +153,8 @@ class DistantDiskLamp: w.write("Color [rec709, %f %f %f]\n" % (col[1][0], col[1][1], col[1][2])) elif col[0] == 'Blackbody': w.write("Color [blackbody, %f %f]\n" % (col[1], col[2])) + elif col[0] == 'ColorTemperature': + w.write("Color [color_temperature, %f %f]\n" % (col[1], col[2])) for rad in self.time_rad: w.write("Radius [%f]\n" % rad) diff --git a/src/color.rs b/src/color.rs index 81698a3..360bb84 100644 --- a/src/color.rs +++ b/src/color.rs @@ -46,6 +46,12 @@ pub enum Color { temperature: f32, // In kelvin factor: f32, // Brightness multiplier }, + // Same as Blackbody except with the spectrum's energy roughly + // normalized. + Temperature { + temperature: f32, // In kelvin + factor: f32, // Brightness multiplier + }, } impl Color { @@ -62,6 +68,14 @@ impl Color { } } + #[inline(always)] + pub fn new_temperature(temp: f32, fac: f32) -> Self { + Color::Temperature { + temperature: temp, + factor: fac, + } + } + pub fn to_spectral_sample(self, hero_wavelength: f32) -> SpectralSample { let wls = wavelengths(hero_wavelength); match self { @@ -84,6 +98,21 @@ impl Color { hero_wavelength, ) } + Color::Temperature { + temperature, + factor, + } => { + SpectralSample::from_parts( + // TODO: make this SIMD + Float4::new( + plancks_law_normalized(temperature, wls.get_0()) * factor, + plancks_law_normalized(temperature, wls.get_1()) * factor, + plancks_law_normalized(temperature, wls.get_2()) * factor, + plancks_law_normalized(temperature, wls.get_3()) * factor, + ), + hero_wavelength, + ) + } } } @@ -91,10 +120,19 @@ impl Color { /// /// Note: this really is very _approximate_. pub fn approximate_energy(self) -> f32 { - // TODO: better approximation for Blackbody. + // TODO: better approximation for Blackbody and Temperature. match self { Color::XYZ(_, y, _) => y, - Color::Blackbody { factor, .. } => factor, + + Color::Blackbody { + temperature, + factor, + } => { + let t2 = temperature * temperature; + t2 * t2 * factor + } + + Color::Temperature { factor, .. } => factor, } } } @@ -105,6 +143,7 @@ impl Mul for Color { fn mul(self, rhs: f32) -> Self { match self { Color::XYZ(x, y, z) => Color::XYZ(x * rhs, y * rhs, z * rhs), + Color::Blackbody { temperature, factor, @@ -112,6 +151,14 @@ impl Mul for Color { temperature: temperature, factor: factor * rhs, }, + + Color::Temperature { + temperature, + factor, + } => Color::Temperature { + temperature: temperature, + factor: factor * rhs, + }, } } } @@ -140,6 +187,7 @@ impl Lerp for Color { (y1 * inv_alpha) + (y2 * alpha), (z1 * inv_alpha) + (z2 * alpha), ), + ( Color::Blackbody { temperature: tmp1, @@ -153,6 +201,21 @@ impl Lerp for Color { temperature: (tmp1 * inv_alpha) + (tmp2 * alpha), factor: (fac1 * inv_alpha) + (fac2 * alpha), }, + + ( + Color::Temperature { + temperature: tmp1, + factor: fac1, + }, + Color::Temperature { + temperature: tmp2, + factor: fac2, + }, + ) => Color::Temperature { + temperature: (tmp1 * inv_alpha) + (tmp2 * alpha), + factor: (fac1 * inv_alpha) + (fac2 * alpha), + }, + _ => panic!("Cannot lerp colors with different representations."), } } @@ -199,6 +262,14 @@ fn plancks_law(temperature: f32, wavelength: f32) -> f32 { (energy * 1.0e-6).max(0.0) } +/// Same as above, except normalized to keep roughly equal spectral +/// energy across temperatures. This makes it easier to use for +/// choosing colors without making brightness explode. +fn plancks_law_normalized(temperature: f32, wavelength: f32) -> f32 { + let t2 = temperature * temperature; + plancks_law(temperature, wavelength) * 4.0e7 / (t2 * t2) +} + //---------------------------------------------------------------- #[derive(Copy, Clone, Debug)] diff --git a/src/parse/psy.rs b/src/parse/psy.rs index f55996b..c2d3210 100644 --- a/src/parse/psy.rs +++ b/src/parse/psy.rs @@ -606,6 +606,16 @@ pub fn parse_color(contents: &str) -> Result { } } + "color_temperature" => { + if let IResult::Done(_, (temperature, factor)) = + closure!(tuple!(ws_f32, ws_f32))(items[1].as_bytes()) + { + return Ok(Color::new_temperature(temperature, factor)); + } else { + return Err(PsyParseError::UnknownError(0)); + } + } + _ => return Err(PsyParseError::UnknownError(0)), } }