mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-01 14:48:12 +08:00
Add MemoizeImpure node and cache image base64 in graph (#1595)
* Cache base64 representation of images when converting to graphic group * Fix build * Fix build again * Actually fix it this time --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
8fa46ba63a
commit
349ec5da72
+9
@@ -1024,6 +1024,15 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||||||
manual_composition: Some(concrete!(())),
|
manual_composition: Some(concrete!(())),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
DocumentNodeDefinition {
|
||||||
|
name: "MemoizeImpure",
|
||||||
|
category: "Structural",
|
||||||
|
implementation: NodeImplementation::proto("graphene_core::memo::ImpureMemoNode<_, _, _>"),
|
||||||
|
inputs: vec![DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true)],
|
||||||
|
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
|
||||||
|
manual_composition: Some(concrete!(Footprint)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
DocumentNodeDefinition {
|
DocumentNodeDefinition {
|
||||||
name: "Image",
|
name: "Image",
|
||||||
category: "Ignore",
|
category: "Ignore",
|
||||||
|
|||||||
@@ -159,7 +159,19 @@ async fn construct_artboard<Fut: Future<Output = GraphicGroup>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl From<ImageFrame<Color>> for GraphicElement {
|
impl From<ImageFrame<Color>> for GraphicElement {
|
||||||
fn from(image_frame: ImageFrame<Color>) -> Self {
|
fn from(mut image_frame: ImageFrame<Color>) -> Self {
|
||||||
|
use base64::Engine;
|
||||||
|
|
||||||
|
let image = &image_frame.image;
|
||||||
|
if !image.data.is_empty() {
|
||||||
|
let output = image.to_png();
|
||||||
|
let preamble = "data:image/png;base64,";
|
||||||
|
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
|
||||||
|
base64_string.push_str(preamble);
|
||||||
|
base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string);
|
||||||
|
image_frame.image.base64_string = Some(base64_string);
|
||||||
|
}
|
||||||
|
|
||||||
GraphicElement::ImageFrame(image_frame)
|
GraphicElement::ImageFrame(image_frame)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
mod quad;
|
mod quad;
|
||||||
|
|
||||||
use crate::raster::{BlendMode, Image, ImageFrame};
|
use crate::raster::{BlendMode, Image, ImageFrame};
|
||||||
|
use crate::transform::Transform;
|
||||||
use crate::uuid::{generate_uuid, ManipulatorGroupId};
|
use crate::uuid::{generate_uuid, ManipulatorGroupId};
|
||||||
use crate::{vector::VectorData, Artboard, Color, GraphicElement, GraphicGroup};
|
use crate::{vector::VectorData, Artboard, Color, GraphicElement, GraphicGroup};
|
||||||
pub use quad::Quad;
|
pub use quad::Quad;
|
||||||
@@ -262,7 +263,16 @@ impl GraphicElementRendered for GraphicGroup {
|
|||||||
self.iter().filter_map(|element| element.bounding_box(transform * self.transform)).reduce(Quad::combine_bounds)
|
self.iter().filter_map(|element| element.bounding_box(transform * self.transform)).reduce(Quad::combine_bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_click_targets(&self, _click_targets: &mut Vec<ClickTarget>) {}
|
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||||
|
for element in self.elements.iter().cloned() {
|
||||||
|
let mut new_click_targets = Vec::new();
|
||||||
|
element.add_click_targets(&mut new_click_targets);
|
||||||
|
for click_target in new_click_targets.iter_mut() {
|
||||||
|
click_target.subpath.apply_transform(element.transform())
|
||||||
|
}
|
||||||
|
click_targets.extend(new_click_targets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn to_usvg_node(&self) -> usvg::Node {
|
fn to_usvg_node(&self) -> usvg::Node {
|
||||||
let root_node = usvg::Node::new(usvg::NodeKind::Group(usvg::Group::default()));
|
let root_node = usvg::Node::new(usvg::NodeKind::Group(usvg::Group::default()));
|
||||||
@@ -453,12 +463,15 @@ impl GraphicElementRendered for ImageFrame<Color> {
|
|||||||
if image.data.is_empty() {
|
if image.data.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let output = image.to_png();
|
|
||||||
let preamble = "data:image/png;base64,";
|
|
||||||
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
|
|
||||||
base64_string.push_str(preamble);
|
|
||||||
base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string);
|
|
||||||
|
|
||||||
|
let base64_string = image.base64_string.clone().unwrap_or_else(|| {
|
||||||
|
let output = image.to_png();
|
||||||
|
let preamble = "data:image/png;base64,";
|
||||||
|
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
|
||||||
|
base64_string.push_str(preamble);
|
||||||
|
base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string);
|
||||||
|
base64_string
|
||||||
|
});
|
||||||
render.leaf_tag("image", |attributes| {
|
render.leaf_tag("image", |attributes| {
|
||||||
attributes.push("width", 1.to_string());
|
attributes.push("width", 1.to_string());
|
||||||
attributes.push("height", 1.to_string());
|
attributes.push("height", 1.to_string());
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use core::cell::Cell;
|
|||||||
use core::marker::PhantomData;
|
use core::marker::PhantomData;
|
||||||
use core::pin::Pin;
|
use core::pin::Pin;
|
||||||
|
|
||||||
// Caches the output of a given Node and acts as a proxy
|
/// Caches the output of a given Node and acts as a proxy
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct MemoNode<T, CachedNode> {
|
pub struct MemoNode<T, CachedNode> {
|
||||||
cache: Cell<Option<T>>,
|
cache: Cell<Option<T>>,
|
||||||
@@ -45,6 +45,53 @@ impl<T, CachedNode> MemoNode<T, CachedNode> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Caches the output of a given Node and acts as a proxy.
|
||||||
|
/// In contrast to the relgular `MemoNode`. This node ignores all input.
|
||||||
|
/// Using this node might result in the document not updating properly,
|
||||||
|
/// use with caution.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ImpureMemoNode<I, T, CachedNode> {
|
||||||
|
cache: Cell<Option<T>>,
|
||||||
|
node: CachedNode,
|
||||||
|
_phantom: std::marker::PhantomData<I>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'i, 'o: 'i, I: 'i, T: 'i + Clone + 'o, CachedNode: 'i> Node<'i, I> for ImpureMemoNode<I, T, CachedNode>
|
||||||
|
where
|
||||||
|
CachedNode: for<'any_input> Node<'any_input, I>,
|
||||||
|
for<'a> <CachedNode as Node<'a, I>>::Output: core::future::Future<Output = T> + 'a,
|
||||||
|
{
|
||||||
|
// TODO: This should return a reference to the cached cached_value
|
||||||
|
// but that requires a lot of lifetime magic <- This was suggested by copilot but is pretty accurate xD
|
||||||
|
type Output = Pin<Box<dyn Future<Output = T> + 'i>>;
|
||||||
|
fn eval(&'i self, input: I) -> Pin<Box<dyn Future<Output = T> + 'i>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if let Some(cached_value) = self.cache.take() {
|
||||||
|
self.cache.set(Some(cached_value.clone()));
|
||||||
|
cached_value
|
||||||
|
} else {
|
||||||
|
let value = self.node.eval(input).await;
|
||||||
|
self.cache.set(Some(value.clone()));
|
||||||
|
value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&self) {
|
||||||
|
self.cache.set(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, I, CachedNode> ImpureMemoNode<I, T, CachedNode> {
|
||||||
|
pub const fn new(node: CachedNode) -> ImpureMemoNode<I, T, CachedNode> {
|
||||||
|
ImpureMemoNode {
|
||||||
|
cache: Cell::new(None),
|
||||||
|
node,
|
||||||
|
_phantom: core::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Stores both what a node was called with and what it returned.
|
/// Stores both what a node was called with and what it returned.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct IORecord<I, O> {
|
pub struct IORecord<I, O> {
|
||||||
|
|||||||
@@ -645,6 +645,7 @@ mod test {
|
|||||||
width: 5,
|
width: 5,
|
||||||
height: 5,
|
height: 5,
|
||||||
data: vec![Color::from_rgbf32_unchecked(1., 0., 0.); 25],
|
data: vec![Color::from_rgbf32_unchecked(1., 0., 0.); 25],
|
||||||
|
base64_string: None,
|
||||||
});
|
});
|
||||||
let image = image.then(ImageRefNode::new());
|
let image = image.then(ImageRefNode::new());
|
||||||
let window = WindowNode::new(radius, image);
|
let window = WindowNode::new(radius, image);
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ pub struct Image<P: Pixel> {
|
|||||||
pub height: u32,
|
pub height: u32,
|
||||||
#[cfg_attr(feature = "serde", serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64"))]
|
#[cfg_attr(feature = "serde", serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64"))]
|
||||||
pub data: Vec<P>,
|
pub data: Vec<P>,
|
||||||
|
/// Optional: Stores a base64 string representation of the image which can be used to speed up the conversion
|
||||||
|
/// to an svg string. This is used as a cache in order to not have to encode the data on every graph evaluation.
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
|
pub base64_string: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: Pixel + Debug> Debug for Image<P> {
|
impl<P: Pixel + Debug> Debug for Image<P> {
|
||||||
@@ -108,6 +112,7 @@ impl<P: Pixel> Image<P> {
|
|||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
data: Vec::new(),
|
data: Vec::new(),
|
||||||
|
base64_string: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +121,7 @@ impl<P: Pixel> Image<P> {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
data: vec![color; (width * height) as usize],
|
data: vec![color; (width * height) as usize],
|
||||||
|
base64_string: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +138,12 @@ impl Image<Color> {
|
|||||||
/// Generate Image from some frontend image data (the canvas pixels as u8s in a flat array)
|
/// Generate Image from some frontend image data (the canvas pixels as u8s in a flat array)
|
||||||
pub fn from_image_data(image_data: &[u8], width: u32, height: u32) -> Self {
|
pub fn from_image_data(image_data: &[u8], width: u32, height: u32) -> Self {
|
||||||
let data = image_data.chunks_exact(4).map(|v| Color::from_rgba8_srgb(v[0], v[1], v[2], v[3])).collect();
|
let data = image_data.chunks_exact(4).map(|v| Color::from_rgba8_srgb(v[0], v[1], v[2], v[3])).collect();
|
||||||
Image { width, height, data }
|
Image {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
data,
|
||||||
|
base64_string: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_png(&self) -> Vec<u8> {
|
pub fn to_png(&self) -> Vec<u8> {
|
||||||
@@ -153,7 +164,7 @@ where
|
|||||||
{
|
{
|
||||||
/// Flattens each channel cast to a u8
|
/// Flattens each channel cast to a u8
|
||||||
pub fn to_flat_u8(&self) -> (Vec<u8>, u32, u32) {
|
pub fn to_flat_u8(&self) -> (Vec<u8>, u32, u32) {
|
||||||
let Image { width, height, data } = self;
|
let Image { width, height, data, .. } = self;
|
||||||
assert_eq!(data.len(), *width as usize * *height as usize);
|
assert_eq!(data.len(), *width as usize * *height as usize);
|
||||||
|
|
||||||
// Cache the last sRGB value we computed, speeds up fills.
|
// Cache the last sRGB value we computed, speeds up fills.
|
||||||
@@ -243,6 +254,7 @@ fn map_node<P: Pixel>(input: (u32, u32), data: Vec<P>) -> Image<P> {
|
|||||||
width: input.0,
|
width: input.0,
|
||||||
height: input.1,
|
height: input.1,
|
||||||
data,
|
data,
|
||||||
|
base64_string: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,6 +391,7 @@ impl From<ImageFrame<Color>> for ImageFrame<SRGBA8> {
|
|||||||
data,
|
data,
|
||||||
width: image.image.width,
|
width: image.image.width,
|
||||||
height: image.image.height,
|
height: image.image.height,
|
||||||
|
base64_string: None,
|
||||||
},
|
},
|
||||||
transform: image.transform,
|
transform: image.transform,
|
||||||
alpha_blending: image.alpha_blending,
|
alpha_blending: image.alpha_blending,
|
||||||
@@ -394,6 +407,7 @@ impl From<ImageFrame<SRGBA8>> for ImageFrame<Color> {
|
|||||||
data,
|
data,
|
||||||
width: image.image.width,
|
width: image.image.width,
|
||||||
height: image.image.height,
|
height: image.image.height,
|
||||||
|
base64_string: None,
|
||||||
},
|
},
|
||||||
transform: image.transform,
|
transform: image.transform,
|
||||||
alpha_blending: image.alpha_blending,
|
alpha_blending: image.alpha_blending,
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ async fn map_gpu<'a: 'input>(image: ImageFrame<Color>, node: DocumentNode, edito
|
|||||||
data: image.image.data.iter().map(|c| quantization::quantize_color(*c, quantization)).collect(),
|
data: image.image.data.iter().map(|c| quantization::quantize_color(*c, quantization)).collect(),
|
||||||
width: image.image.width,
|
width: image.image.width,
|
||||||
height: image.image.height,
|
height: image.image.height,
|
||||||
|
base64_string: None,
|
||||||
},
|
},
|
||||||
transform: image.transform,
|
transform: image.transform,
|
||||||
alpha_blending: image.alpha_blending,
|
alpha_blending: image.alpha_blending,
|
||||||
@@ -140,6 +141,7 @@ async fn map_gpu<'a: 'input>(image: ImageFrame<Color>, node: DocumentNode, edito
|
|||||||
data: colors,
|
data: colors,
|
||||||
width: image.image.width,
|
width: image.image.width,
|
||||||
height: image.image.height,
|
height: image.image.height,
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
transform: image.transform,
|
transform: image.transform,
|
||||||
alpha_blending: image.alpha_blending,
|
alpha_blending: image.alpha_blending,
|
||||||
@@ -586,6 +588,7 @@ async fn blend_gpu_image(foreground: ImageFrame<Color>, background: ImageFrame<C
|
|||||||
data: colors,
|
data: colors,
|
||||||
width: background.image.width,
|
width: background.image.width,
|
||||||
height: background.image.height,
|
height: background.image.height,
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
transform: background.transform,
|
transform: background.transform,
|
||||||
alpha_blending: background.alpha_blending,
|
alpha_blending: background.alpha_blending,
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ mod test {
|
|||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
data: vec![Color::from_rgbaf32(0.0, 0.0, 0.0, 1.0).unwrap(); 10000],
|
data: vec![Color::from_rgbaf32(0.0, 0.0, 0.0, 1.0).unwrap(); 10000],
|
||||||
|
base64_string: None,
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -446,7 +446,7 @@ async fn imaginate_maybe_fail<'a, P: Pixel, F: Fn(ImaginateStatus)>(
|
|||||||
fn image_to_base64<P: Pixel>(image: Image<P>) -> Result<String, Error> {
|
fn image_to_base64<P: Pixel>(image: Image<P>) -> Result<String, Error> {
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
|
|
||||||
let Image { width, height, data } = image;
|
let Image { width, height, data, .. } = image;
|
||||||
|
|
||||||
fn cast_with_f32<S: Pixel, D: image::Pixel<Subpixel = f32>>(data: Vec<S>, width: u32, height: u32) -> Result<DynamicImage, Error>
|
fn cast_with_f32<S: Pixel, D: image::Pixel<Subpixel = f32>>(data: Vec<S>, width: u32, height: u32) -> Result<DynamicImage, Error>
|
||||||
where
|
where
|
||||||
@@ -485,7 +485,12 @@ fn base64_to_image<D: AsRef<[u8]>, P: Pixel>(base64_data: D) -> Result<Image<P>,
|
|||||||
_ => return Err(Error::UnsupportedPixelType(core::any::type_name::<P>())),
|
_ => return Err(Error::UnsupportedPixelType(core::any::type_name::<P>())),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Image { data: result_data, width, height })
|
Ok(Image {
|
||||||
|
data: result_data,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
base64_string: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pick_safe_imaginate_resolution((width, height): (f64, f64)) -> (u64, u64) {
|
pub fn pick_safe_imaginate_resolution((width, height): (f64, f64)) -> (u64, u64) {
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ fn sample(footprint: Footprint, image_frame: ImageFrame<Color>) -> ImageFrame<Co
|
|||||||
width: new_width,
|
width: new_width,
|
||||||
height: new_height,
|
height: new_height,
|
||||||
data: vec,
|
data: vec,
|
||||||
|
base64_string: None,
|
||||||
};
|
};
|
||||||
// we need to adjust the offset if we truncate the offset calculation
|
// we need to adjust the offset if we truncate the offset calculation
|
||||||
|
|
||||||
@@ -750,7 +751,12 @@ fn mandelbrot_node(footprint: Footprint) -> ImageFrame<Color> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImageFrame {
|
ImageFrame {
|
||||||
image: Image { width, height, data },
|
image: Image {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
data,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
|
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ fn decode_image_node<'a: 'input>(data: Arc<[u8]>) -> ImageFrame<Color> {
|
|||||||
data: image.chunks(4).map(|pixel| Color::from_unassociated_alpha(pixel[0], pixel[1], pixel[2], pixel[3])).collect(),
|
data: image.chunks(4).map(|pixel| Color::from_unassociated_alpha(pixel[0], pixel[1], pixel[2], pixel[3])).collect(),
|
||||||
width: image.width(),
|
width: image.width(),
|
||||||
height: image.height(),
|
height: image.height(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -652,6 +652,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: wgpu_executor::WgpuSurface, params: [wgpu_executor::WgpuSurface]),
|
async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: wgpu_executor::WgpuSurface, params: [wgpu_executor::WgpuSurface]),
|
||||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: SurfaceFrame, params: [SurfaceFrame]),
|
async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: SurfaceFrame, params: [SurfaceFrame]),
|
||||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: RenderOutput, params: [RenderOutput]),
|
async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: RenderOutput, params: [RenderOutput]),
|
||||||
|
async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Footprint, output: GraphicGroup, fn_params: [Footprint => GraphicGroup]),
|
||||||
|
async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData]),
|
||||||
register_node!(graphene_core::structural::ConsNode<_, _>, input: Image<Color>, params: [&str]),
|
register_node!(graphene_core::structural::ConsNode<_, _>, input: Image<Color>, params: [&str]),
|
||||||
register_node!(graphene_std::raster::ImageFrameNode<_, _>, input: Image<Color>, params: [DAffine2]),
|
register_node!(graphene_std::raster::ImageFrameNode<_, _>, input: Image<Color>, params: [DAffine2]),
|
||||||
register_node!(graphene_std::raster::NoisePatternNode<_, _, _, _, _, _, _, _, _, _, _, _, _, _, _>, input: (), params: [UVec2, u32, f32, NoiseType, DomainWarpType, f32, FractalType, u32, f32, f32, f32, f32, CellularDistanceFunction, CellularReturnType, f32]),
|
register_node!(graphene_std::raster::NoisePatternNode<_, _, _, _, _, _, _, _, _, _, _, _, _, _, _>, input: (), params: [UVec2, u32, f32, NoiseType, DomainWarpType, f32, FractalType, u32, f32, f32, f32, f32, CellularDistanceFunction, CellularReturnType, f32]),
|
||||||
|
|||||||
Reference in New Issue
Block a user