implement shadows
This commit is contained in:
parent
0000039063
commit
0000040001
12 changed files with 213 additions and 61 deletions
39
assignment-1b/Cargo.lock
generated
39
assignment-1b/Cargo.lock
generated
|
@ -28,6 +28,7 @@ dependencies = [
|
||||||
"num",
|
"num",
|
||||||
"ordered-float",
|
"ordered-float",
|
||||||
"rayon",
|
"rayon",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -388,6 +389,12 @@ version = "1.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
|
checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-error"
|
name = "proc-macro-error"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
@ -532,6 +539,38 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing"
|
||||||
|
version = "0.1.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tracing-attributes",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-attributes"
|
||||||
|
version = "0.1.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-core"
|
||||||
|
version = "0.1.30"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
|
|
|
@ -16,3 +16,4 @@ nalgebra = "0.32.1"
|
||||||
num = { version = "0.4.0", features = ["serde"] }
|
num = { version = "0.4.0", features = ["serde"] }
|
||||||
ordered-float = "3.4.0"
|
ordered-float = "3.4.0"
|
||||||
rayon = "1.6.1"
|
rayon = "1.6.1"
|
||||||
|
tracing = "0.1.37"
|
||||||
|
|
|
@ -5,7 +5,7 @@ hfov 60
|
||||||
updir 0 1 0
|
updir 0 1 0
|
||||||
bkgcolor 0.1 0.1 0.1
|
bkgcolor 0.1 0.1 0.1
|
||||||
|
|
||||||
depthcueing 0.1 0.1 0.1 1 0.2 100 5
|
depthcueing 0.1 0.1 0.1 1 0.2 100 0
|
||||||
|
|
||||||
light -10 10 -3 0 0.8 0.8 0.8
|
light -10 10 -3 0 0.8 0.8 0.8
|
||||||
light -10 10 -3 1 0.8 0.8 0.8
|
light -10 10 -3 1 0.8 0.8 0.8
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
extern crate anyhow;
|
extern crate anyhow;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate derivative;
|
extern crate derivative;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate tracing;
|
||||||
|
|
||||||
pub mod image;
|
pub mod image;
|
||||||
pub mod ray;
|
pub mod ray;
|
||||||
|
|
|
@ -98,12 +98,13 @@ fn main() -> Result<()> {
|
||||||
let intersections = scene
|
let intersections = scene
|
||||||
.objects
|
.objects
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|object| {
|
.enumerate()
|
||||||
|
.filter_map(|(i, object)| {
|
||||||
match object.kind.intersects_ray_at(&ray) {
|
match object.kind.intersects_ray_at(&ray) {
|
||||||
Ok(Some(t)) => {
|
Ok(Some(t)) => {
|
||||||
// Return both the t and the sphere, because we want to sort on
|
// Return both the t and the sphere, because we want to sort on
|
||||||
// the t but later retrieve attributes from the sphere
|
// the t but later retrieve attributes from the sphere
|
||||||
Some(Ok((t, object)))
|
Some(Ok((i, t, object)))
|
||||||
}
|
}
|
||||||
Ok(None) => None,
|
Ok(None) => None,
|
||||||
Err(e) => Some(Err(e)),
|
Err(e) => Some(Err(e)),
|
||||||
|
@ -113,13 +114,12 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
// Sort the list of intersection times by the lowest one.
|
// Sort the list of intersection times by the lowest one.
|
||||||
let earliest_intersection =
|
let earliest_intersection =
|
||||||
intersections.into_iter().min_by_key(|(t, _)| t.time);
|
intersections.into_iter().min_by_key(|(_, t, _)| t.time);
|
||||||
|
|
||||||
Ok(match earliest_intersection {
|
Ok(match earliest_intersection {
|
||||||
// Take the object's material color
|
// Take the object's material color
|
||||||
Some((intersection_context, object)) => {
|
Some((obj_idx, intersection_context, object)) => scene
|
||||||
scene.compute_pixel_color(object.material, intersection_context)
|
.compute_pixel_color(obj_idx, object.material, intersection_context),
|
||||||
}
|
|
||||||
|
|
||||||
// There was no intersection, so this should default to the scene's
|
// There was no intersection, so this should default to the scene's
|
||||||
// background color
|
// background color
|
||||||
|
|
|
@ -15,12 +15,12 @@ pub struct Cylinder {
|
||||||
pub length: f64,
|
pub length: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjectKind for Cylinder {
|
impl Cylinder {
|
||||||
/// Given a cylinder, returns the first time at which this ray intersects the
|
/// Given a cylinder, returns the first time at which this ray intersects the
|
||||||
/// cylinder.
|
/// cylinder.
|
||||||
///
|
///
|
||||||
/// If there is no intersection point, returns None.
|
/// If there is no intersection point, returns None.
|
||||||
fn intersects_ray_at(
|
pub fn intersects_ray_at(
|
||||||
&self,
|
&self,
|
||||||
ray: &Ray,
|
ray: &Ray,
|
||||||
) -> Result<Option<IntersectionContext>> {
|
) -> Result<Option<IntersectionContext>> {
|
||||||
|
|
|
@ -6,23 +6,38 @@ use nalgebra::Vector3;
|
||||||
use crate::image::Color;
|
use crate::image::Color;
|
||||||
use crate::ray::Ray;
|
use crate::ray::Ray;
|
||||||
|
|
||||||
|
use super::cylinder::Cylinder;
|
||||||
use super::illumination::IntersectionContext;
|
use super::illumination::IntersectionContext;
|
||||||
|
use super::sphere::Sphere;
|
||||||
use super::Scene;
|
use super::Scene;
|
||||||
|
|
||||||
pub trait ObjectKind: Debug + Send + Sync {
|
#[derive(Debug)]
|
||||||
|
pub enum ObjectKind {
|
||||||
|
Sphere(Sphere),
|
||||||
|
Cylinder(Cylinder),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectKind {
|
||||||
/// Determine where the ray intersects this object, returning the earliest
|
/// Determine where the ray intersects this object, returning the earliest
|
||||||
/// time this happens. Returns None if no intersection occurs.
|
/// time this happens. Returns None if no intersection occurs.
|
||||||
///
|
///
|
||||||
/// Also known as Trace_Ray in the slides, except not the part where it calls
|
/// Also known as Trace_Ray in the slides, except not the part where it calls
|
||||||
/// Shade_Ray.
|
/// Shade_Ray.
|
||||||
fn intersects_ray_at(&self, ray: &Ray)
|
pub fn intersects_ray_at(
|
||||||
-> Result<Option<IntersectionContext>>;
|
&self,
|
||||||
|
ray: &Ray,
|
||||||
|
) -> Result<Option<IntersectionContext>> {
|
||||||
|
match self {
|
||||||
|
ObjectKind::Sphere(sphere) => sphere.intersects_ray_at(ray),
|
||||||
|
ObjectKind::Cylinder(cylinder) => cylinder.intersects_ray_at(ray),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An object in the scene
|
/// An object in the scene
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Object {
|
pub struct Object {
|
||||||
pub kind: Box<dyn ObjectKind>,
|
pub kind: ObjectKind,
|
||||||
|
|
||||||
/// Index into the scene's material color list
|
/// Index into the scene's material color list
|
||||||
pub material: usize,
|
pub material: usize,
|
||||||
|
@ -66,12 +81,36 @@ pub struct Light {
|
||||||
pub color: Vector3<f64>,
|
pub color: Vector3<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Light {
|
||||||
|
/// Get the unit directional vector pointing from the given point to this
|
||||||
|
/// light source
|
||||||
|
pub fn direction_from(&self, point: Vector3<f64>) -> Vector3<f64> {
|
||||||
|
match self.kind {
|
||||||
|
LightKind::Point { location } => location - point,
|
||||||
|
LightKind::Directional { direction } => -direction,
|
||||||
|
}
|
||||||
|
.normalize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DepthCueing {
|
pub struct DepthCueing {
|
||||||
|
/// The color to tint (should be the same as the background color, to avoid
|
||||||
|
/// bizarre visual effects)
|
||||||
pub color: Color,
|
pub color: Color,
|
||||||
|
|
||||||
|
/// Proportion of the color influenced by the depth tint when the distance is
|
||||||
|
/// maxed (caps at 1.0)
|
||||||
pub a_max: f64,
|
pub a_max: f64,
|
||||||
|
|
||||||
|
/// Proportion of the color influenced by the depth tint when the distance is
|
||||||
|
/// at the minimum (caps at 1.0)
|
||||||
pub a_min: f64,
|
pub a_min: f64,
|
||||||
|
|
||||||
|
/// The max distance that should be affected by the depth tint
|
||||||
pub dist_max: f64,
|
pub dist_max: f64,
|
||||||
|
|
||||||
|
/// The min distance that should be affected by the depth tint
|
||||||
pub dist_min: f64,
|
pub dist_min: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,36 +1,16 @@
|
||||||
|
use anyhow::Result;
|
||||||
use nalgebra::Vector3;
|
use nalgebra::Vector3;
|
||||||
use ordered_float::NotNan;
|
use ordered_float::NotNan;
|
||||||
|
use rayon::prelude::{
|
||||||
use crate::image::Color;
|
IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator,
|
||||||
|
|
||||||
use super::{
|
|
||||||
data::{DepthCueing, LightKind},
|
|
||||||
Scene,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Information about an intersection
|
use crate::{image::Color, ray::Ray};
|
||||||
#[derive(Derivative)]
|
|
||||||
#[derivative(Debug, PartialEq, PartialOrd, Ord)]
|
|
||||||
pub struct IntersectionContext {
|
|
||||||
/// The time of the intersection in the parametric ray
|
|
||||||
///
|
|
||||||
/// Unfortunately, IEEE floats in Rust don't have total ordering, because
|
|
||||||
/// NaNs violate ordering properties. The way to remedy this is to ensure we
|
|
||||||
/// don't have NaNs by wrapping it into this type, which then implements
|
|
||||||
/// total ordering.
|
|
||||||
pub time: NotNan<f64>,
|
|
||||||
|
|
||||||
/// The intersection point.
|
use super::{
|
||||||
#[derivative(PartialEq = "ignore", Ord = "ignore")]
|
data::{DepthCueing, Light, LightKind},
|
||||||
pub point: Vector3<f64>,
|
Scene,
|
||||||
|
};
|
||||||
/// The normal vector protruding from the surface of the object at the
|
|
||||||
/// intersection point
|
|
||||||
#[derivative(PartialEq = "ignore", Ord = "ignore")]
|
|
||||||
pub normal: Vector3<f64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for IntersectionContext {}
|
|
||||||
|
|
||||||
impl Scene {
|
impl Scene {
|
||||||
/// Determine the color that should be used to fill this pixel.
|
/// Determine the color that should be used to fill this pixel.
|
||||||
|
@ -42,6 +22,7 @@ impl Scene {
|
||||||
/// Also known as Shade_Ray in the slides.
|
/// Also known as Shade_Ray in the slides.
|
||||||
pub fn compute_pixel_color(
|
pub fn compute_pixel_color(
|
||||||
&self,
|
&self,
|
||||||
|
obj_idx: usize,
|
||||||
material_idx: usize,
|
material_idx: usize,
|
||||||
intersection_context: IntersectionContext,
|
intersection_context: IntersectionContext,
|
||||||
) -> Color {
|
) -> Color {
|
||||||
|
@ -57,16 +38,10 @@ impl Scene {
|
||||||
// Diffuse and specular lighting for each separate light
|
// Diffuse and specular lighting for each separate light
|
||||||
let diffuse_and_specular: Vector3<f64> = self
|
let diffuse_and_specular: Vector3<f64> = self
|
||||||
.lights
|
.lights
|
||||||
.iter()
|
.par_iter()
|
||||||
.map(|light| {
|
.map(|light| {
|
||||||
// The vector pointing in the direction of the light
|
// The vector pointing in the direction of the light
|
||||||
let light_direction = match light.kind {
|
let light_direction = light.direction_from(intersection_context.point);
|
||||||
LightKind::Point { location } => {
|
|
||||||
location - intersection_context.point
|
|
||||||
}
|
|
||||||
LightKind::Directional { direction } => -direction,
|
|
||||||
}
|
|
||||||
.normalize();
|
|
||||||
|
|
||||||
let normal = intersection_context.normal.normalize();
|
let normal = intersection_context.normal.normalize();
|
||||||
let viewer_direction = self.eye_pos - intersection_context.point;
|
let viewer_direction = self.eye_pos - intersection_context.point;
|
||||||
|
@ -84,8 +59,14 @@ impl Scene {
|
||||||
.max(0.0)
|
.max(0.0)
|
||||||
.powf(material.exponent);
|
.powf(material.exponent);
|
||||||
|
|
||||||
|
let shadow_flag =
|
||||||
|
match self.in_shadow_of(obj_idx, intersection_context.point, light) {
|
||||||
|
true => 0.0,
|
||||||
|
false => 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
let diffuse_and_specular = diffuse_component + specular_component;
|
let diffuse_and_specular = diffuse_component + specular_component;
|
||||||
light.color.component_mul(&diffuse_and_specular)
|
shadow_flag * light.color.component_mul(&diffuse_and_specular)
|
||||||
})
|
})
|
||||||
.sum();
|
.sum();
|
||||||
|
|
||||||
|
@ -119,4 +100,93 @@ impl Scene {
|
||||||
|
|
||||||
clamped_result
|
clamped_result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform another ray casting to see if there are any objects obstructing
|
||||||
|
/// the light source to this particular point
|
||||||
|
pub fn in_shadow_of(
|
||||||
|
&self,
|
||||||
|
obj_idx: usize,
|
||||||
|
point: Vector3<f64>,
|
||||||
|
light: &Light,
|
||||||
|
) -> bool {
|
||||||
|
let light_direction = light.direction_from(point);
|
||||||
|
let ray = Ray {
|
||||||
|
origin: point,
|
||||||
|
direction: light_direction.normalize(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Small helper for iterating over all of the objects in the scene except
|
||||||
|
// for the current one
|
||||||
|
let other_objects = self
|
||||||
|
.objects
|
||||||
|
.par_iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, _)| *i != obj_idx);
|
||||||
|
|
||||||
|
// Get the list of intersections with all the other objects in the scene
|
||||||
|
let intersections = other_objects
|
||||||
|
.filter_map(|(_, object)| {
|
||||||
|
let intersection_context = match object.kind.intersects_ray_at(&ray) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Error while performing shadow casting: {err}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
|
||||||
|
match light.kind {
|
||||||
|
// In the case of point lights, we must check to see if both t > 0 and
|
||||||
|
// t is less than the time it took to even get to the light.
|
||||||
|
LightKind::Point { location } => {
|
||||||
|
let light_time = (location - ray.origin).norm();
|
||||||
|
let intersection_time = *intersection_context.time;
|
||||||
|
|
||||||
|
if intersection_time <= 0.0 || intersection_time >= light_time {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(intersection_context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the case of directional lights, only t > 0 needs to be checked,
|
||||||
|
// otherwise
|
||||||
|
LightKind::Directional { .. } => {
|
||||||
|
if *intersection_context.time <= 0.0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(intersection_context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
!intersections.is_empty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Information about an intersection
|
||||||
|
#[derive(Derivative)]
|
||||||
|
#[derivative(Debug, PartialEq, PartialOrd, Ord)]
|
||||||
|
pub struct IntersectionContext {
|
||||||
|
/// The time of the intersection in the parametric ray
|
||||||
|
///
|
||||||
|
/// Unfortunately, IEEE floats in Rust don't have total ordering, because
|
||||||
|
/// NaNs violate ordering properties. The way to remedy this is to ensure we
|
||||||
|
/// don't have NaNs by wrapping it into this type, which then implements
|
||||||
|
/// total ordering.
|
||||||
|
pub time: NotNan<f64>,
|
||||||
|
|
||||||
|
/// The intersection point.
|
||||||
|
#[derivative(PartialEq = "ignore", Ord = "ignore")]
|
||||||
|
pub point: Vector3<f64>,
|
||||||
|
|
||||||
|
/// The normal vector protruding from the surface of the object at the
|
||||||
|
/// intersection point
|
||||||
|
#[derivative(PartialEq = "ignore", Ord = "ignore")]
|
||||||
|
pub normal: Vector3<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for IntersectionContext {}
|
||||||
|
|
||||||
|
impl IntersectionContext {}
|
||||||
|
|
|
@ -10,7 +10,7 @@ use crate::scene::{
|
||||||
Scene,
|
Scene,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::data::DepthCueing;
|
use super::data::{DepthCueing, ObjectKind};
|
||||||
|
|
||||||
impl Scene {
|
impl Scene {
|
||||||
/// Parse the input file into a scene
|
/// Parse the input file into a scene
|
||||||
|
@ -125,7 +125,7 @@ impl Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
"sphere" => scene.objects.push(Object {
|
"sphere" => scene.objects.push(Object {
|
||||||
kind: Box::new(Sphere {
|
kind: ObjectKind::Sphere(Sphere {
|
||||||
center: read_vec3(0)?,
|
center: read_vec3(0)?,
|
||||||
radius: parts[3],
|
radius: parts[3],
|
||||||
}),
|
}),
|
||||||
|
@ -138,7 +138,7 @@ impl Scene {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
"cylinder" => scene.objects.push(Object {
|
"cylinder" => scene.objects.push(Object {
|
||||||
kind: Box::new(Cylinder {
|
kind: ObjectKind::Cylinder(Cylinder {
|
||||||
center: read_vec3(0)?,
|
center: read_vec3(0)?,
|
||||||
direction: read_vec3(3)?,
|
direction: read_vec3(3)?,
|
||||||
radius: parts[6],
|
radius: parts[6],
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
pub mod cylinder;
|
pub mod cylinder;
|
||||||
pub mod data;
|
pub mod data;
|
||||||
|
pub mod illumination;
|
||||||
pub mod input_file;
|
pub mod input_file;
|
||||||
pub mod sphere;
|
pub mod sphere;
|
||||||
pub mod illumination;
|
|
||||||
|
|
||||||
use nalgebra::Vector3;
|
use nalgebra::Vector3;
|
||||||
|
|
||||||
|
|
|
@ -12,12 +12,12 @@ pub struct Sphere {
|
||||||
pub radius: f64,
|
pub radius: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjectKind for Sphere {
|
impl Sphere {
|
||||||
/// Given a sphere, returns the first time at which this ray intersects the
|
/// Given a sphere, returns the first time at which this ray intersects the
|
||||||
/// sphere.
|
/// sphere.
|
||||||
///
|
///
|
||||||
/// If there is no intersection point, returns None.
|
/// If there is no intersection point, returns None.
|
||||||
fn intersects_ray_at(
|
pub fn intersects_ray_at(
|
||||||
&self,
|
&self,
|
||||||
ray: &Ray,
|
ray: &Ray,
|
||||||
) -> Result<Option<IntersectionContext>> {
|
) -> Result<Option<IntersectionContext>> {
|
||||||
|
|
11
flake.nix
11
flake.nix
|
@ -13,14 +13,15 @@
|
||||||
in rec {
|
in rec {
|
||||||
devShell = pkgs.mkShell {
|
devShell = pkgs.mkShell {
|
||||||
packages = (with pkgs; [
|
packages = (with pkgs; [
|
||||||
cargo-watch
|
|
||||||
cargo-deny
|
cargo-deny
|
||||||
cargo-edit
|
cargo-edit
|
||||||
pandoc
|
cargo-flamegraph
|
||||||
zip
|
cargo-watch
|
||||||
unzip
|
|
||||||
texlive.combined.scheme-full
|
|
||||||
imagemagick
|
imagemagick
|
||||||
|
pandoc
|
||||||
|
texlive.combined.scheme-full
|
||||||
|
unzip
|
||||||
|
zip
|
||||||
|
|
||||||
(python310.withPackages (p: with p; [ numpy ]))
|
(python310.withPackages (p: with p; [ numpy ]))
|
||||||
]) ++ (with toolchain; [
|
]) ++ (with toolchain; [
|
||||||
|
|
Loading…
Reference in a new issue