ft (wip): textures

This commit is contained in:
2026-05-12 18:20:06 +02:00
parent 383f739808
commit 6a1e50fb7a
13 changed files with 222 additions and 6 deletions

34
scenes/texture.json Normal file
View File

@@ -0,0 +1,34 @@
{
"filename": "textured.png",
"image_width": 1920,
"image_height": 1080,
"max_depth": 50,
"camera": {
"anti_alias_rate": 2,
"fov": 50.0,
"look_from": [-2, 4, 5],
"look_at": [-2, 0.0, 0.0],
"vup": [0.0, 1.0, 0.0], "defocus_angle": 0, "focus_dist": 15.68
},
"materials": [
{ "type": "lambertian", "albedo": [0.2, 0.2, 0.2], "prob": 0.8 },
{ "type": "lambertian", "albedo": [0.9, 0.9, 0.0], "prob": 1.0, "fuzz": 0.1 },
{ "type": "dielectric", "refraction_index": 1.5},
{ "type": "normal"},
{"type": "texture", "source": "./textures/earthmap1k.png"},
{"type": "texture", "source": "./textures/yellow.png"}
],
"objects": [
{ "type": "sphere", "center": [0.0, 0.0, -1.2], "radius": 0.9, "material": 5},
{ "type": "sphere", "center": [-2, 0, -1], "radius": 0.8, "material": 4},
{ "type": "triangle", "p1": [0, 0, -4], "p2": [4, 0, -4], "p3": [2, 2, -4], "material": 5},
{ "type": "triangle", "p1": [-2, 2, -4], "p2": [2, 2, -4], "p3": [0, 4, -4], "material": 4},
{ "type": "quad", "p1": [-20, -1, -20], "p2": [20, -1, -20], "p3": [20, 20, -20], "p4": [-20, 20, -20], "material": 0},
{ "type": "quad", "p1": [-20, -1, 20], "p2": [-20, -1, -20], "p3": [-20, 20, -20], "p4": [-20, 20, 20], "material": 0},
{ "type": "quad", "p1": [-20, -1, 20], "p2": [20, -1, 20], "p3": [20, -1, -20], "p4": [-20, -1, -20], "material": 0},
{ "type": "quad", "p1": [20, -1, 20], "p2": [20, -1, -20], "p3": [20, 20, -20], "p4": [20, 20, 20], "material": 0}
]
}

View File

@@ -1,4 +1,4 @@
use std::sync::Arc;
use std::{f32::consts::PI, sync::Arc};
use crate::{
objects::{materials::traits::Material, traits::Hittable},
@@ -45,8 +45,27 @@ impl Hittable for Circle {
let p = r.at(t);
if (p - self.center).length() < self.radius {
return Some(Hit::new(t, p, self.normal, self.material.clone(), self.normal.dot(&r.dir()) < 0.));
let uv = self.to_uv(&p);
return Some(Hit::new(
t,
p,
self.normal,
self.material.clone(),
self.normal.dot(&r.dir()) < 0.,
*uv.x(),
*uv.y(),
));
}
None
}
fn to_uv(&self, point: &Vec3) -> Vec3 {
let p = *point - self.center;
// TODO: add rotated texture support
return Vec3::new(
0.5 + p.y().atan2(*p.x()) / (2. * PI),
1. - (p.z() / self.radius).acos() / PI,
0.0,
);
}
}

View File

@@ -41,4 +41,9 @@ impl Hittable for Cube {
fn hit(&self, r: &Ray) -> Option<Hit> {
Hit::hit_list(&self.faces, r)
}
fn to_uv(&self, point: &Vec3) -> Vec3 {
// TODO: map to [0.1] relative to specific face
todo!()
}
}

View File

@@ -23,4 +23,8 @@ impl Hittable for Cylinder {
fn hit(&self, r: &crate::ray::Ray) -> Option<super::hit::Hit> {
todo!()
}
fn to_uv(&self, point: &Vec3) -> Vec3 {
todo!()
}
}

View File

@@ -12,16 +12,20 @@ pub struct Hit {
n: Vec3,
mat: Arc<dyn Material>,
front_face: bool,
u: f32,
v: f32,
}
impl Hit {
pub fn new(t: f32, p: Vec3, n: Vec3, mat: Arc<dyn Material>, front_face: bool) -> Self {
pub fn new(t: f32, p: Vec3, n: Vec3, mat: Arc<dyn Material>, front_face: bool, u: f32, v: f32) -> Self {
Self {
t,
p,
n: if front_face { n } else { -n },
mat,
front_face,
u,
v
}
}
@@ -45,6 +49,10 @@ impl Hit {
self.front_face
}
pub fn uv(&self) -> (f32, f32) {
(self.u, self.v)
}
// TODO: use front_face to discard back-hits for culling
pub fn hit_list(hittables: &Vec<Arc<dyn Hittable>>, r: &Ray) -> Option<Hit> {
let mut closest: Option<Hit> = None;

View File

@@ -2,3 +2,4 @@ pub mod dielectric;
pub mod lambertian;
pub mod normal;
pub mod traits;
pub mod texture;

View File

@@ -0,0 +1,47 @@
use image::{DynamicImage, ImageReader};
use crate::{
objects::{hit::Hit, materials::traits::Material},
ray::Ray,
vec3::Vec3,
};
#[derive(Debug)]
pub struct Texture {
source: DynamicImage,
width: u32,
height: u32,
}
impl Texture {
pub fn new(texture: &str) -> Self {
let img = ImageReader::open(texture).unwrap().decode().unwrap(); // FIXME: unwraps
Self {
width: img.width(),
height: img.height(),
source: img,
}
}
fn _idx(&self, u: f32, v: f32) -> usize {
let x: usize = (u * ((self.width - 1) as f32)) as usize; // TODO: check these calcs work
let y: usize = (v * ((self.height - 1) as f32)) as usize;
y * self.width as usize + x
}
fn _at(&self, u: f32, v: f32) -> Vec3 {
let b = self.source.as_bytes(); // FIXME: i think im indexing colours incorrectly
let idx = self._idx(u, v);
Vec3::from_u8(b[3 * idx], b[3 * idx + 1], b[3 * idx + 2])
}
}
impl Material for Texture {
fn scatter(&self, hit: &Hit, _ray: &Ray) -> Option<(Option<Ray>, Vec3)> {
let (u, v) = hit.uv();
let col = self._at(u, 1. - v);
return Some((None, col));
}
}

View File

@@ -69,4 +69,10 @@ impl Hittable for Quad {
r,
)
}
fn to_uv(&self, point: &Vec3) -> Vec3 {
let u = (*point - self.p1).dot(&(self.p2 - self.p1)) / (self.p2 - self.p1).length_squared();
let v = (*point - self.p1).dot(&(self.p4 - self.p1)) / (self.p4 - self.p1).length_squared();
Vec3::new(u, v, 0.)
}
}

View File

@@ -1,4 +1,5 @@
use core::f32::math::sqrt;
use std::f32::consts::PI;
use std::fmt::{self, Debug};
use std::sync::Arc;
@@ -14,7 +15,6 @@ pub struct Sphere {
material: Arc<dyn Material>,
}
impl Debug for Sphere {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Sphere")
@@ -59,13 +59,26 @@ impl Hittable for Sphere {
let t = if tl > 0.001 { tl } else { tr };
let p = r.at(t);
let out_n = (p - self.center) / self.radius;
let uv = self.to_uv(&p);
Some(Hit::new(
t,
p,
(p - self.center).get_unit(),
self.material.clone(),
out_n.dot(r.dir()) < 0.,
*uv.x(),
*uv.y(),
))
}
}
fn to_uv(&self, point: &Vec3) -> Vec3 {
let p = *point - self.center;
// TODO: add rotated texture support
return Vec3::new(
0.5 + p.y().atan2(*p.x()) / (2. * PI),
1. - (p.z() / self.radius).acos() / PI,
0.0,
);
}
}

View File

@@ -2,7 +2,10 @@ use std::fmt::Debug;
use crate::Ray;
use crate::objects::hit::Hit;
use crate::vec3::Vec3;
pub trait Hittable: Debug + Send + Sync {
fn hit(&self, r: &Ray) -> Option<Hit>;
fn to_uv(&self, point: &Vec3) -> Vec3; // TODO: overhaul; remove u,v in Hit and change Mat ref to
// Hittable ref. Then call this function to calculate u,v coords as necessary.
}

View File

@@ -11,19 +11,60 @@ pub struct Triangle {
p3: Vec3,
material: Arc<dyn Material>,
normal: Vec3,
// barycentric helpers
v0: Vec3,
v1: Vec3,
d00: f32,
d01: f32,
d11: f32,
denom: f32,
}
impl Triangle {
pub fn new(p1: Vec3, p2: Vec3, p3: Vec3, material: Arc<dyn Material>) -> Self {
// barycentric helpers cached to save computations
let v0 = p2 - p1;
let v1 = p3 - p1;
let d00 = v0.dot(&v0);
let d01 = v0.dot(&v1);
let d11 = v1.dot(&v1);
Self {
p1,
p2,
p3,
material,
normal: (p2 - p1).cross(&(p3 - p1)).get_unit(),
v0,
v1,
d00,
d01,
d11,
denom: d00 * d11 - d01 * d01,
}
}
// TODO: make the global Triangle::hit function not take precomped u,v somehow
// FIXME/DEBT: this function recalculates values for each pass that can be cached on the
// triangle struct itself. REFACTOR to not use this function
pub fn to_uv(p1: &Vec3, p2: &Vec3, p3: &Vec3, point: &Vec3) -> Vec3 {
let v0 = *p2 - p1;
let v1 = *p3 - p1;
let v2 = *point - p1;
let d00 = v0.dot(&v0);
let d01 = v0.dot(&v1);
let d11 = v1.dot(&v1);
let denom = d00 * d11 - d01 * d01;
let d20 = v2.dot(&v0);
let d21 = v2.dot(&v1);
let v = (d11 * d20 - d01 * d21) / denom;
let w = (d00 * d21 - d01 * d20) / denom;
Vec3::new(1. - v - w, v, w)
}
pub fn hit(
p1: Vec3,
p2: Vec3,
@@ -45,6 +86,7 @@ impl Triangle {
if t < 0. {
return None;
};
let uvw = Triangle::to_uv(&p1, &p2, &p3, &r.at(t));
let p = r.at(t);
let a4 = (p3 - p1).cross(&(p2 - p1)).length();
@@ -54,7 +96,15 @@ impl Triangle {
let diff = (a4 - a1 - a2 - a3).abs();
if diff < 0.001 {
Some(Hit::new(t, p, normal, material, normal.dot(&r.dir()) < 0.))
Some(Hit::new(
t,
p,
normal,
material,
normal.dot(&r.dir()) < 0.,
*uvw.x(),
*uvw.y(),
))
} else {
None
}
@@ -83,4 +133,14 @@ impl Hittable for Triangle {
r,
)
}
fn to_uv(&self, point: &Vec3) -> Vec3 {
let v2 = *point - self.p1;
let d20 = v2.dot(&self.v0);
let d21 = v2.dot(&self.v1);
let v = (self.d11 * d20 - self.d01 * d21) / self.denom;
let w = (self.d00 * d21 - self.d01 * d20) / self.denom;
Vec3::new(1. - v - w, v, w)
}
}

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use serde::Deserialize;
use crate::objects::materials::{dielectric::Dielectric, lambertian::{Lambertian, Metal}, normal::Normal, traits::Material};
use crate::objects::materials::{dielectric::Dielectric, lambertian::{Lambertian, Metal}, normal::Normal, texture::Texture, traits::Material};
#[derive(Deserialize)]
@@ -12,6 +12,7 @@ pub(crate) enum MaterialDef {
Metal(Metal),
Dielectric(Dielectric),
Normal(Normal),
Texture(RawTexture),
}
impl MaterialDef {
@@ -21,7 +22,13 @@ impl MaterialDef {
MaterialDef::Metal(m) => Arc::new(m),
MaterialDef::Dielectric(d) => Arc::new(d),
MaterialDef::Normal(n) => Arc::new(n),
MaterialDef::Texture(t) => Arc::new(Texture::new(&t.source)), // FIXME: error handling
}
}
}
#[derive(Deserialize)]
pub(crate) struct RawTexture {
pub source: String,
}

View File

@@ -29,6 +29,14 @@ impl Vec3 {
}
}
pub fn from_u8(r: u8, b: u8, g: u8) -> Self {
Self {
x: r as f32 / 255.,
y: g as f32 / 255.,
z: b as f32/ 255.,
}
}
pub fn random() -> Self {
let mut rng = rand::rng();
Self {
@@ -492,3 +500,4 @@ impl Display for Vec3 {
write!(f, "({}, {}, {})", self.x, self.y, self.z)
}
}