Render: integrated workspace, preview image jobs and opaque PBR (#1046)
Lands the Render workspace (R3), bounded preview-image jobs with cancellation and atomic PNG export (R4a), and opaque PBR looks as one change. - Render workspace: an overlay-free product preview on the installed GPU device. Returning to Design restores its camera. render.scene and render.settings.apply are added. - Image jobs: render.preflight/start/status/cancel/export run bounded CPU preview jobs. Export captures the job, overwrite choice and document, and checks the caller's file scope before writing. - Opaque PBR: looks can carry metal fraction, roughness and dielectric IOR. The product viewport and preview PNG use them; geometry and mass are unchanged. The v1 catalog export refuses PBR rather than dropping it. - Render setups and PBR looks are saved under design format 21. Labels are in en/de/ja/ko/zh/ru. Supersedes #706 and #800. Refs #620, #673, #756, #1004. Contributed by @0xlillith. Co-authored-by: lillith <[email protected]>
This commit is contained in:
1 parent
e4ef6cfe29
commit
a261aaa841
70 files changed
+5860
-101
No files matched your search
@@ -12,11 +12,34 @@ use crate::Document;
|
||||
|
||||
/// A look: a name (a library entry's, or "Custom"), sRGB colour and opacity (1 = opaque).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "LookWire")]
|
||||
pub struct Look {
|
||||
pub name: String,
|
||||
pub color: [u8; 3],
|
||||
#[serde(default = "opaque")]
|
||||
pub opacity: f64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub pbr: Option<crate::pbr_appearance::PbrParameters>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LookWire {
|
||||
name: String,
|
||||
color: [u8; 3],
|
||||
#[serde(default = "opaque")]
|
||||
opacity: f64,
|
||||
#[serde(default)]
|
||||
pbr: Option<crate::pbr_appearance::PbrParameters>,
|
||||
}
|
||||
|
||||
impl TryFrom<LookWire> for Look {
|
||||
type Error = String;
|
||||
fn try_from(value: LookWire) -> std::result::Result<Self, Self::Error> {
|
||||
if value.pbr.is_some() && value.opacity != 1.0 {
|
||||
return Err("opaque PBR requires opacity 1; refraction/transmission is not supported".into());
|
||||
}
|
||||
Ok(Self { name: value.name, color: value.color, opacity: value.opacity, pbr: value.pbr })
|
||||
}
|
||||
}
|
||||
|
||||
fn opaque() -> f64 {
|
||||
@@ -25,7 +48,13 @@ fn opaque() -> f64 {
|
||||
|
||||
impl Look {
|
||||
pub fn custom(color: [u8; 3], opacity: f64) -> Look {
|
||||
Look { name: "Custom".into(), color, opacity: opacity.clamp(0.0, 1.0) }
|
||||
Look { name: "Custom".into(), color, opacity: opacity.clamp(0.0, 1.0), pbr: None }
|
||||
}
|
||||
pub fn opaque_surface(&self) -> crate::Result<Option<solidcraft_render::surface::OpaqueSurface>> {
|
||||
if self.pbr.is_some() && self.opacity != 1.0 {
|
||||
return Err(crate::DocError::Invalid("opaque PBR requires opacity 1".into()));
|
||||
}
|
||||
self.pbr.map(|p| p.surface(self.color).map_err(|e| crate::DocError::Invalid(e.to_string()))).transpose()
|
||||
}
|
||||
/// Colour as 0–1 fractions.
|
||||
pub fn rgb(&self) -> [f32; 3] {
|
||||
@@ -109,14 +138,19 @@ pub use defaults::LIBRARY;
|
||||
|
||||
/// A library appearance by name (case-insensitive).
|
||||
pub fn library(name: &str) -> Option<Look> {
|
||||
LIBRARY.iter().find(|(n, _, _)| n.eq_ignore_ascii_case(name.trim())).map(|(n, c, o)| Look { name: n.to_string(), color: *c, opacity: *o })
|
||||
LIBRARY.iter().find(|(n, _, _)| n.eq_ignore_ascii_case(name.trim())).map(|(n, c, o)| Look {
|
||||
name: n.to_string(),
|
||||
color: *c,
|
||||
opacity: *o,
|
||||
pbr: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The look a physical material gives a body that has no appearance of its own.
|
||||
pub fn material_look(material: &str) -> Option<Look> {
|
||||
let color = crate::material_color(material)?;
|
||||
let opacity = if material == "Glass" { 0.25 } else { 1.0 };
|
||||
Some(Look { name: material.to_string(), color, opacity })
|
||||
Some(Look { name: material.to_string(), color, opacity, pbr: None })
|
||||
}
|
||||
|
||||
impl Document {
|
||||
|
||||
@@ -164,6 +164,9 @@
|
||||
//! (`com.example.tool`). Every key is kept as it is, and nothing in it is checked when the
|
||||
//! design opens. A design without it saves as before. 0.11.x refuses format 21, so no
|
||||
//! released build drops the properties when it saves.
|
||||
//! Under the same version, looks can explicitly select opaque PBR parameters (`pbr`: metal
|
||||
//! fraction, roughness, dielectric IOR). Absent fields keep the scalar shading a look always
|
||||
//! had; 0.11.0 asks for an update instead of silently dropping the response.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod appearance;
|
||||
mod assembly;
|
||||
mod body_transfer;
|
||||
mod body_transfer_sheet;
|
||||
pub mod pbr_appearance;
|
||||
pub use body_transfer::{valid_transfer_matrix, validate_body_transfer};
|
||||
pub mod canvas;
|
||||
mod clipboard;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Persistable opaque optical parameters, independent of engineering material data.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solidcraft_geom::Vec3;
|
||||
use solidcraft_render::surface::{OpaqueSurface, SurfaceError, srgb8_to_linear};
|
||||
|
||||
/// Explicitly selected opaque response. Existing scalar looks have no such record.
|
||||
/// Construction and deserialization share the renderer's finite admission bounds.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "Parameters", into = "Parameters")]
|
||||
pub struct PbrParameters {
|
||||
metallic: f64,
|
||||
roughness: f64,
|
||||
dielectric_ior: f64,
|
||||
}
|
||||
|
||||
impl Default for PbrParameters {
|
||||
fn default() -> Self {
|
||||
Self { metallic: 0.0, roughness: 0.5, dielectric_ior: 1.5 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Parameters {
|
||||
metallic: f64,
|
||||
roughness: f64,
|
||||
dielectric_ior: f64,
|
||||
}
|
||||
|
||||
impl PbrParameters {
|
||||
pub fn new(metallic: f64, roughness: f64, dielectric_ior: f64) -> Result<Self, SurfaceError> {
|
||||
OpaqueSurface::new(Vec3::ZERO, metallic, roughness, dielectric_ior)?;
|
||||
Ok(Self { metallic, roughness, dielectric_ior })
|
||||
}
|
||||
|
||||
/// Decode the containing look's sRGB8 base color once, for the actual consumer.
|
||||
pub fn surface(self, color: [u8; 3]) -> Result<OpaqueSurface, SurfaceError> {
|
||||
let [r, g, b] = color.map(srgb8_to_linear);
|
||||
OpaqueSurface::new(Vec3::new(r, g, b), self.metallic, self.roughness, self.dielectric_ior)
|
||||
}
|
||||
|
||||
pub fn metallic(self) -> f64 {
|
||||
self.metallic
|
||||
}
|
||||
pub fn roughness(self) -> f64 {
|
||||
self.roughness
|
||||
}
|
||||
pub fn dielectric_ior(self) -> f64 {
|
||||
self.dielectric_ior
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Parameters> for PbrParameters {
|
||||
type Error = SurfaceError;
|
||||
fn try_from(p: Parameters) -> Result<Self, Self::Error> {
|
||||
Self::new(p.metallic, p.roughness, p.dielectric_ior)
|
||||
}
|
||||
}
|
||||
impl From<PbrParameters> for Parameters {
|
||||
fn from(p: PbrParameters) -> Self {
|
||||
Self { metallic: p.metallic, roughness: p.roughness, dielectric_ior: p.dielectric_ior }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn optical_parameters_share_strict_admission_and_round_trip() {
|
||||
let p = PbrParameters::new(1.0, 0.3, 1.5).unwrap();
|
||||
let saved = serde_json::to_value(p).unwrap();
|
||||
assert_eq!(serde_json::from_value::<PbrParameters>(saved.clone()).unwrap(), p);
|
||||
for (key, value) in [("metallic", json!(1.1)), ("roughness", json!(0)), ("dielectric_ior", json!(0.9)), ("extra", json!(true))] {
|
||||
let mut bad = saved.clone();
|
||||
bad[key] = value;
|
||||
assert!(serde_json::from_value::<PbrParameters>(bad).is_err());
|
||||
}
|
||||
assert!(PbrParameters::new(f64::NAN, 0.3, 1.5).is_err());
|
||||
assert!(PbrParameters::new(0.0, f64::INFINITY, 1.5).is_err());
|
||||
let surface = p.surface([128, 0, 255]).unwrap();
|
||||
assert!((surface.base_color().x - 0.21586050011389926).abs() < 1e-14);
|
||||
assert_eq!(surface.base_color().z, 1.0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user