bots-garden/mini-mepublic Fork
⑂ e41fbe6
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

🛟 Updated.

k33g committed 2026-11-13T13:12:42+01:00 Browse files
e41fbe6 parent: a334114
deleted demos/qwen-style/workspace/HANDOFF.md +0 -118
deleted file mode 100644
@@ -1,118 +0,0 @@
1-# Handoff — état du projet `povray`
2-
3-Dernière mise à jour : après le path tracing et le tutoriel.
4-Ce fichier permet de reprendre le travail sans re-lire toute l'historique.
5-
6-## Ce qu'est le projet
7-
8-Bibliothèque Go **sans dépendances** qui rend des scènes façon POV-Ray :
9-on décrit objets, matériaux, lumières et caméra en code Go, la
10-bibliothèque trace des rayons et produit une image PNG.
11-
12-- Module : `github.com/user/povray` (**placeholder** — voir "À faire")
13-- Go 1.27, encodeur PNG maison (stdlib only), rendu parallèle en
14- chunks de lignes (goroutines + `sync.WaitGroup`)
15-
16-## État : ce qui marche
17-
18-Tout est vérifié : `gofmt` propre, `go vet` propre sur la bibliothèque,
19-`go test ./...` passe, les 5 exemples rendent des PNG 800×600 valides.
20-
21-| Fichier | Rôle |
22-|---|---|
23-| `vector.go` | `Vec3`, `Ray`, `Hit`, `Flip` |
24-| `objects.go` | interface `Object` ; `Sphere`, `Plane`, `Box`, `Group` |
25-| `material.go` | `Color` (+ presets nommés), `Material` (dont `Emission`), `Light`, `LightMaterial` |
26-| `camera.go` | caméra trou d'épingle ; `RayAt` (centre pixel) et `RayAtUV` (point fractionnaire, pour le supersampling) |
27-| `transform.go` | `Mat4` 4×4 : `Translate`/`RotateX/Y/Z`/`Scale`/`Multiply`/`Inverse` (Gauss-Jordan) ; `Transform{Object, Matrix}` |
28-| `renderer.go` | `Scene`, `Renderer` (réflexions/réfractions, ombres, `Samples` = grille n×n), `Image`, PPM |
29-| `pathtracer.go` | `PathTracer` : GI, ombres douces, cosinus sampling, russe roulette, déterministe (graines PCG par goroutine) |
30-| `png.go` | encodeur PNG maison, `Image.SavePNG` |
31-| `povray_test.go` | tests unitaires + smoke tests de rendu (dont `TestSupersampling`, `TestPathTracer`) |
32-| `examples/` | `spheres`, `mirror`, `procedural`, `transform`, `pathtrace` — chacun écrit son `.png` |
33-| `docs/TUTORIEL.md` | guide FR en 7 niveaux (0→6) pour composer de belles images |
34-| `README.md` | référence API (EN) |
35-| `ROADMAP.md` | ce qui manque, classé par impact |
36-
37-## Décisions importantes (ne pas re-discuter sans raison)
38-
39-- **Zéro dépendance** : PNG écrit à la main (`compress/zlib`, `hash/crc32`,
40- `encoding/binary`). PPM reste dispo.
41-- **Littéraux non-keyed** `Vec3{1, 0, 0}` / `Color{...}` : style
42- bibliothèque de maths, assumé ; `go vet` le signale dans les exemples,
43- c'est documenté dans le README.
44-- **Ordre des transformations** : `Multiply` s'applique de droite à
45- gauche (inside-out) — `Translate(...).Multiply(RotateX(...))` =
46- rotation d'abord. Documenté dans le tutoriel (niveau 3).
47-- **Normales sous transformation** : transposée de l'inverse (correct
48- sous scale non uniforme), renormalisées.
49-- **Path tracer** : lumières = surfaces émissives uniquement
50- (`LightMaterial`) ; `Scene.Lights` ignorées. `Samples` = rayons/pixel
51- (pas une grille). Déterministe par design.
52-- **Ombres** : les objets avec `Transparency > 0.5` ne portent pas
53- d'ombre (documenté).
54-- **Gamma 2.2** appliqué dans `Image.Set` (toujours, pas optionnel).
55-- **Tutoriel en français**, README en anglais — volontaire.
56-
57-## Pièges connus (déjà tombés dedans, ne pas re-tomber)
58-
59-- `RayAtUV` prend des `float64` : `px+0.5` sur un `int` est une
60- troncature silencieuse (vet le catche). Toujours `float64(px)+0.5`.
61-- `Hit.Dist` est un `float64` : `.Len()` pas `.Norm()` (qui renvoie un
62- `Vec3`).
63-- **Boucle infinie de rebonds** : verre/miroir peuvent rebondir
64- indéfiniment → stack overflow. Le `PathTracer` a une limite dure de
65- 50 (le `Renderer` est borné par `MaxDepth`). Tout nouveau type de
66- rebond doit respecter une limite.
67-- `Box` : les normales doivent pointer vers l'extérieur (le `Copysign`
68- est négatif dans `objects.go` — ne pas "corriger" en le retournant).
69-- Le `Transform` avec `Matrix` zéro (non initialisé) est singulier :
70- `Inverse` renvoie `ok=false`, l'objet est simplement invisible.
71- Utiliser `povray.Identity()` si on ne transforme pas.
72-- Tests de rendu : les couleurs après gamma ne sont pas les valeurs
73- attendues naïvement (ex. gris 0.5 → 186, pas 128). Mesurer avant
74- d'écrire les seuils.
75-- IEND d'un PNG : length(4) à `len-12`, "IEND" à `len-8`, CRC
76- `AE426082` à `len-4`.
77-
78-## Commandes utiles
79-
80-```sh
81-cd workspace
82-go test ./... # tous les tests
83-go vet . # propre sur la bibliothèque
84-go build ./...
85-go run ./examples/spheres # ~1 s
86-go run ./examples/transform # ~1 s
87-go run ./examples/pathtrace # ~40 s (64 samples/pixel)
88-```
89-
90-## À faire (ordre suggéré, détail dans ROADMAP.md)
91-
92-Les 3 gros items du rendu sont faits (transformations, anti-aliasing,
93-path tracing). Reste :
94-
95-1. **Hygiène de package** (~1 h, rend le projet publiable) :
96- - module path réel (remplacer `github.com/user/povray` — demander à
97- l'utilisateur son identifiant GitHub)
98- - `LICENSE` (MIT ou Apache — demander laquelle)
99- - CI GitHub Actions (`go test` + `go vet` + `gofmt -l`)
100- - `Makefile` (`test`, `render`, `clean`)
101- - doc de package (`// Package povray ...` dans un fichier)
102-2. **Textures par position** (checker, gradient, bruit) — le matériau
103- est encore une couleur fixe.
104-3. **Maillage triangles** (import `.obj`) — la primitive manquante pour
105- les vraies scènes.
106-4. Divers : torus/cylindre/cône, normal mapping, importance sampling
107- des lumières, gamma optionnel.
108-
109-## Si on reprend
110-
111-1. Lire ce fichier.
112-2. `go test ./...` pour vérifier l'état de base.
113-3. Choisir un item "À faire" (demander à l'utilisateur s'il n'a pas
114- déjà tranché).
115-4. Le tutoriel (`docs/TUTORIEL.md`) est la référence de ce que l'API
116- doit continuer à rendre lisible — toute évolution d'API doit être
117- reflétée dedans, dans le README et dans le ROADMAP (les trois
118- restent synchronisés).
deleted file mode 100644
@@ -1,118 +0,0 @@
1-# Handoff — état du projet `povray`
2-
3-Dernière mise à jour : après le path tracing et le tutoriel.
4-Ce fichier permet de reprendre le travail sans re-lire toute l'historique.
5-
6-## Ce qu'est le projet
7-
8-Bibliothèque Go **sans dépendances** qui rend des scènes façon POV-Ray :
9-on décrit objets, matériaux, lumières et caméra en code Go, la
10-bibliothèque trace des rayons et produit une image PNG.
11-
12-- Module : `github.com/user/povray` (**placeholder** — voir "À faire")
13-- Go 1.27, encodeur PNG maison (stdlib only), rendu parallèle en
14- chunks de lignes (goroutines + `sync.WaitGroup`)
15-
16-## État : ce qui marche
17-
18-Tout est vérifié : `gofmt` propre, `go vet` propre sur la bibliothèque,
19-`go test ./...` passe, les 5 exemples rendent des PNG 800×600 valides.
20-
21-| Fichier | Rôle |
22-|---|---|
23-| `vector.go` | `Vec3`, `Ray`, `Hit`, `Flip` |
24-| `objects.go` | interface `Object` ; `Sphere`, `Plane`, `Box`, `Group` |
25-| `material.go` | `Color` (+ presets nommés), `Material` (dont `Emission`), `Light`, `LightMaterial` |
26-| `camera.go` | caméra trou d'épingle ; `RayAt` (centre pixel) et `RayAtUV` (point fractionnaire, pour le supersampling) |
27-| `transform.go` | `Mat4` 4×4 : `Translate`/`RotateX/Y/Z`/`Scale`/`Multiply`/`Inverse` (Gauss-Jordan) ; `Transform{Object, Matrix}` |
28-| `renderer.go` | `Scene`, `Renderer` (réflexions/réfractions, ombres, `Samples` = grille n×n), `Image`, PPM |
29-| `pathtracer.go` | `PathTracer` : GI, ombres douces, cosinus sampling, russe roulette, déterministe (graines PCG par goroutine) |
30-| `png.go` | encodeur PNG maison, `Image.SavePNG` |
31-| `povray_test.go` | tests unitaires + smoke tests de rendu (dont `TestSupersampling`, `TestPathTracer`) |
32-| `examples/` | `spheres`, `mirror`, `procedural`, `transform`, `pathtrace` — chacun écrit son `.png` |
33-| `docs/TUTORIEL.md` | guide FR en 7 niveaux (0→6) pour composer de belles images |
34-| `README.md` | référence API (EN) |
35-| `ROADMAP.md` | ce qui manque, classé par impact |
36-
37-## Décisions importantes (ne pas re-discuter sans raison)
38-
39-- **Zéro dépendance** : PNG écrit à la main (`compress/zlib`, `hash/crc32`,
40- `encoding/binary`). PPM reste dispo.
41-- **Littéraux non-keyed** `Vec3{1, 0, 0}` / `Color{...}` : style
42- bibliothèque de maths, assumé ; `go vet` le signale dans les exemples,
43- c'est documenté dans le README.
44-- **Ordre des transformations** : `Multiply` s'applique de droite à
45- gauche (inside-out) — `Translate(...).Multiply(RotateX(...))` =
46- rotation d'abord. Documenté dans le tutoriel (niveau 3).
47-- **Normales sous transformation** : transposée de l'inverse (correct
48- sous scale non uniforme), renormalisées.
49-- **Path tracer** : lumières = surfaces émissives uniquement
50- (`LightMaterial`) ; `Scene.Lights` ignorées. `Samples` = rayons/pixel
51- (pas une grille). Déterministe par design.
52-- **Ombres** : les objets avec `Transparency > 0.5` ne portent pas
53- d'ombre (documenté).
54-- **Gamma 2.2** appliqué dans `Image.Set` (toujours, pas optionnel).
55-- **Tutoriel en français**, README en anglais — volontaire.
56-
57-## Pièges connus (déjà tombés dedans, ne pas re-tomber)
58-
59-- `RayAtUV` prend des `float64` : `px+0.5` sur un `int` est une
60- troncature silencieuse (vet le catche). Toujours `float64(px)+0.5`.
61-- `Hit.Dist` est un `float64` : `.Len()` pas `.Norm()` (qui renvoie un
62- `Vec3`).
63-- **Boucle infinie de rebonds** : verre/miroir peuvent rebondir
64- indéfiniment → stack overflow. Le `PathTracer` a une limite dure de
65- 50 (le `Renderer` est borné par `MaxDepth`). Tout nouveau type de
66- rebond doit respecter une limite.
67-- `Box` : les normales doivent pointer vers l'extérieur (le `Copysign`
68- est négatif dans `objects.go` — ne pas "corriger" en le retournant).
69-- Le `Transform` avec `Matrix` zéro (non initialisé) est singulier :
70- `Inverse` renvoie `ok=false`, l'objet est simplement invisible.
71- Utiliser `povray.Identity()` si on ne transforme pas.
72-- Tests de rendu : les couleurs après gamma ne sont pas les valeurs
73- attendues naïvement (ex. gris 0.5 → 186, pas 128). Mesurer avant
74- d'écrire les seuils.
75-- IEND d'un PNG : length(4) à `len-12`, "IEND" à `len-8`, CRC
76- `AE426082` à `len-4`.
77-
78-## Commandes utiles
79-
80-```sh
81-cd workspace
82-go test ./... # tous les tests
83-go vet . # propre sur la bibliothèque
84-go build ./...
85-go run ./examples/spheres # ~1 s
86-go run ./examples/transform # ~1 s
87-go run ./examples/pathtrace # ~40 s (64 samples/pixel)
88-```
89-
90-## À faire (ordre suggéré, détail dans ROADMAP.md)
91-
92-Les 3 gros items du rendu sont faits (transformations, anti-aliasing,
93-path tracing). Reste :
94-
95-1. **Hygiène de package** (~1 h, rend le projet publiable) :
96- - module path réel (remplacer `github.com/user/povray` — demander à
97- l'utilisateur son identifiant GitHub)
98- - `LICENSE` (MIT ou Apache — demander laquelle)
99- - CI GitHub Actions (`go test` + `go vet` + `gofmt -l`)
100- - `Makefile` (`test`, `render`, `clean`)
101- - doc de package (`// Package povray ...` dans un fichier)
102-2. **Textures par position** (checker, gradient, bruit) — le matériau
103- est encore une couleur fixe.
104-3. **Maillage triangles** (import `.obj`) — la primitive manquante pour
105- les vraies scènes.
106-4. Divers : torus/cylindre/cône, normal mapping, importance sampling
107- des lumières, gamma optionnel.
108-
109-## Si on reprend
110-
111-1. Lire ce fichier.
112-2. `go test ./...` pour vérifier l'état de base.
113-3. Choisir un item "À faire" (demander à l'utilisateur s'il n'a pas
114- déjà tranché).
115-4. Le tutoriel (`docs/TUTORIEL.md`) est la référence de ce que l'API
116- doit continuer à rendre lisible — toute évolution d'API doit être
117- reflétée dedans, dans le README et dans le ROADMAP (les trois
118- restent synchronisés).
deleted demos/qwen-style/workspace/README.md +0 -127
deleted file mode 100644
@@ -1,127 +0,0 @@
1-# povray — a small Go ray tracer
2-
3-A dependency-free Go library for rendering scenes the way POV-Ray does:
4-you describe objects, materials, lights and a camera in Go code, and the
5-library traces rays to produce an image.
6-
7-```
8-workspace/
9-├── go.mod
10-├── HANDOFF.md state snapshot for resuming work later
11-├── vector.go Vec3 math, Ray, Hit
12-├── objects.go Sphere, Plane, Box, Group + the Object interface
13-├── material.go Color, Material, Light
14-├── camera.go pinhole Camera
15-├── transform.go Mat4 (translate/rotate/scale, inverse) + Transform object
16-├── renderer.go Scene, Renderer, Image, PPM writer
17-├── pathtracer.go PathTracer (global illumination, soft shadows)
18-├── png.go Image.SavePNG (dependency-free PNG encoder)
19-├── docs/
20-│ └── TUTORIEL.md step-by-step guide (French) for making good images
21-└── examples/
22- ├── spheres/ basic diffuse/specular scene
23- ├── mirror/ reflections + a glass sphere
24- ├── procedural/ a scene generated in loops (hue sweep grid)
25- ├── transform/ tilted boxes, ellipsoid, ring of rotated boxes
26- └── pathtrace/ path-traced room with emissive light panels
27-```
28-
29-## Quick start
30-
31-```go
32-scene := povray.NewScene()
33-scene.AddObject(povray.Sphere{Center: povray.Vec3{0, 1, 0}, Radius: 1},
34- povray.Material{Diffuse: povray.Red, Specular: povray.White, Shininess: 60, Ambient: 0.1})
35-scene.AddObject(povray.NewPlaneXY(0),
36- povray.Material{Diffuse: povray.Gray, Ambient: 0.3})
37-scene.Lights = []povray.Light{povray.NewLight(povray.Vec3{4, 8, 6}, povray.White)}
38-
39-cam := povray.NewCamera(povray.Vec3{0, 2.5, 10})
40-r := povray.Renderer{Scene: scene, Width: 800, Height: 600, Samples: 2}
41-img := r.Render(cam)
42-img.SavePNG("out.png") // or img.SavePPM("out.ppm")
43-```
44-
45-## Concepts
46-
47-- **Objects** implement `Object` (`Intersect(Ray) (Hit, bool)`). Built in:
48- `Sphere`, `Plane` (infinite), `Box` (axis-aligned), `Group` (closest of
49- many). Add your own by implementing the interface.
50-- **Transforms** place any object anywhere: `Translate`, `RotateX/Y/Z`,
51- `Scale` build a `Mat4` (composed with `Multiply`, applied inside-out),
52- and `Transform{Object, Matrix}` maps the ray into the object's local
53- space, intersects there, and maps the hit point and normal back to
54- world space — normals go through the inverse transpose, so non-uniform
55- scale stays correct.
56-- **Materials** control the look: `Diffuse` (base color), `Specular` +
57- `Shininess` (highlights), `Ambient`, `Reflectivity` (mirror, 0..1) and
58- `Transparency` + `IOR` (glass, 0..1).
59-- **Lights** are colored point lights with inverse-square falloff; the
60- renderer casts shadow rays for each.
61-- **Camera** is a pinhole camera: position, look-at point, up vector and
62- vertical field of view.
63-- **Renderer** is parallel (row chunks across goroutines) and limits
64- reflection/refraction bounces with `Scene.MaxDepth`. Set `Samples`
65- for anti-aliasing: `Samples: 2` shoots a 2×2 grid of rays per pixel
66- and averages them (0 or 1 = one ray per pixel, no anti-aliasing).
67-- **PathTracer** renders the same scenes with path tracing: rays
68- bounce between surfaces, giving global illumination and soft
69- shadows. Lights are emissive surfaces (`LightMaterial(color,
70- intensity)`); `Scene.Lights` are ignored. `Samples` is the number of
71- rays per pixel (16 for previews, 64+ for clean output). Renders are
72- deterministic.
73-
74-## Run the examples
75-
76-```sh
77-go run ./examples/spheres # writes spheres.png
78-go run ./examples/mirror # writes mirror.png
79-go run ./examples/procedural # writes procedural.png
80-go run ./examples/transform # writes transform.png
81-go run ./examples/pathtrace # writes pathtrace.png (slower)
82-```
83-
84-For a step-by-step guide to composing good-looking scenes (materials,
85-camera, transforms, path tracing, color recipes), see
86-[docs/TUTORIEL.md](docs/TUTORIEL.md).
87-
88-Output is PNG (8-bit truecolor, written by the library's own encoder — no
89-dependencies). `SavePPM` is still available if you prefer PPM.
90-
91-## Inspiration
92-
93-The architecture and math follow the classic educational ray-tracing books;
94-the Go idioms are ours.
95-
96-- **Ray Tracing in One Weekend** (Peter Shirley) — the main influence:
97- the `Vec3`/`Ray`/`Hit` model, the quadratic-discriminant sphere
98- intersection, and the incremental build-up (sphere → ground → light →
99- specular → shadows → reflections → refraction).
100-- **POV-Ray** — the library's name and scene model come from it: describe
101- objects, materials, lights and a camera; the engine traces rays.
102-- **The Ray Tracer Challenge** (Shirley) — the unit-test style,
103- validating each primitive with simple geometric cases (head-on hit,
104- miss, ray from inside).
105-
106-Specific to this implementation (not in the books, which are C++/Java):
107-
108-- the Go `Object` interface with `Group` as a generic combinator
109-- parallel rendering in row chunks across goroutines (`sync.WaitGroup`)
110-- the hand-rolled PNG encoder (`png.go`) — the books only write PPM
111-- inverse-square light falloff with clamping in `shade`
112-
113-If you want to go further, *Ray Tracing in One Weekend* (and its sequel,
114-*Realistic Rendering with Path Tracing*) is the reference to read — this
115-codebase closely follows its progression.
116-
117-## Limitations
118-
119-No textures, no area lights (emissive surfaces are the light source for
120-the path tracer), no triangle meshes. Anti-aliasing is grid
121-supersampling (`Renderer.Samples`) plus stochastic sampling in the
122-`PathTracer`. Transforms are per-object (`Transform` wrapper); there is
123-no scene-wide model/view matrix yet.
124-
125-Note: `Vec3` and `Color` are intentionally written unkeyed
126-(`povray.Vec3{1, 0, 0}`) — `go vet` will flag this in example code; that's
127-expected for a math library.
deleted file mode 100644
@@ -1,127 +0,0 @@
1-# povray — a small Go ray tracer
2-
3-A dependency-free Go library for rendering scenes the way POV-Ray does:
4-you describe objects, materials, lights and a camera in Go code, and the
5-library traces rays to produce an image.
6-
7-```
8-workspace/
9-├── go.mod
10-├── HANDOFF.md state snapshot for resuming work later
11-├── vector.go Vec3 math, Ray, Hit
12-├── objects.go Sphere, Plane, Box, Group + the Object interface
13-├── material.go Color, Material, Light
14-├── camera.go pinhole Camera
15-├── transform.go Mat4 (translate/rotate/scale, inverse) + Transform object
16-├── renderer.go Scene, Renderer, Image, PPM writer
17-├── pathtracer.go PathTracer (global illumination, soft shadows)
18-├── png.go Image.SavePNG (dependency-free PNG encoder)
19-├── docs/
20-│ └── TUTORIEL.md step-by-step guide (French) for making good images
21-└── examples/
22- ├── spheres/ basic diffuse/specular scene
23- ├── mirror/ reflections + a glass sphere
24- ├── procedural/ a scene generated in loops (hue sweep grid)
25- ├── transform/ tilted boxes, ellipsoid, ring of rotated boxes
26- └── pathtrace/ path-traced room with emissive light panels
27-```
28-
29-## Quick start
30-
31-```go
32-scene := povray.NewScene()
33-scene.AddObject(povray.Sphere{Center: povray.Vec3{0, 1, 0}, Radius: 1},
34- povray.Material{Diffuse: povray.Red, Specular: povray.White, Shininess: 60, Ambient: 0.1})
35-scene.AddObject(povray.NewPlaneXY(0),
36- povray.Material{Diffuse: povray.Gray, Ambient: 0.3})
37-scene.Lights = []povray.Light{povray.NewLight(povray.Vec3{4, 8, 6}, povray.White)}
38-
39-cam := povray.NewCamera(povray.Vec3{0, 2.5, 10})
40-r := povray.Renderer{Scene: scene, Width: 800, Height: 600, Samples: 2}
41-img := r.Render(cam)
42-img.SavePNG("out.png") // or img.SavePPM("out.ppm")
43-```
44-
45-## Concepts
46-
47-- **Objects** implement `Object` (`Intersect(Ray) (Hit, bool)`). Built in:
48- `Sphere`, `Plane` (infinite), `Box` (axis-aligned), `Group` (closest of
49- many). Add your own by implementing the interface.
50-- **Transforms** place any object anywhere: `Translate`, `RotateX/Y/Z`,
51- `Scale` build a `Mat4` (composed with `Multiply`, applied inside-out),
52- and `Transform{Object, Matrix}` maps the ray into the object's local
53- space, intersects there, and maps the hit point and normal back to
54- world space — normals go through the inverse transpose, so non-uniform
55- scale stays correct.
56-- **Materials** control the look: `Diffuse` (base color), `Specular` +
57- `Shininess` (highlights), `Ambient`, `Reflectivity` (mirror, 0..1) and
58- `Transparency` + `IOR` (glass, 0..1).
59-- **Lights** are colored point lights with inverse-square falloff; the
60- renderer casts shadow rays for each.
61-- **Camera** is a pinhole camera: position, look-at point, up vector and
62- vertical field of view.
63-- **Renderer** is parallel (row chunks across goroutines) and limits
64- reflection/refraction bounces with `Scene.MaxDepth`. Set `Samples`
65- for anti-aliasing: `Samples: 2` shoots a 2×2 grid of rays per pixel
66- and averages them (0 or 1 = one ray per pixel, no anti-aliasing).
67-- **PathTracer** renders the same scenes with path tracing: rays
68- bounce between surfaces, giving global illumination and soft
69- shadows. Lights are emissive surfaces (`LightMaterial(color,
70- intensity)`); `Scene.Lights` are ignored. `Samples` is the number of
71- rays per pixel (16 for previews, 64+ for clean output). Renders are
72- deterministic.
73-
74-## Run the examples
75-
76-```sh
77-go run ./examples/spheres # writes spheres.png
78-go run ./examples/mirror # writes mirror.png
79-go run ./examples/procedural # writes procedural.png
80-go run ./examples/transform # writes transform.png
81-go run ./examples/pathtrace # writes pathtrace.png (slower)
82-```
83-
84-For a step-by-step guide to composing good-looking scenes (materials,
85-camera, transforms, path tracing, color recipes), see
86-[docs/TUTORIEL.md](docs/TUTORIEL.md).
87-
88-Output is PNG (8-bit truecolor, written by the library's own encoder — no
89-dependencies). `SavePPM` is still available if you prefer PPM.
90-
91-## Inspiration
92-
93-The architecture and math follow the classic educational ray-tracing books;
94-the Go idioms are ours.
95-
96-- **Ray Tracing in One Weekend** (Peter Shirley) — the main influence:
97- the `Vec3`/`Ray`/`Hit` model, the quadratic-discriminant sphere
98- intersection, and the incremental build-up (sphere → ground → light →
99- specular → shadows → reflections → refraction).
100-- **POV-Ray** — the library's name and scene model come from it: describe
101- objects, materials, lights and a camera; the engine traces rays.
102-- **The Ray Tracer Challenge** (Shirley) — the unit-test style,
103- validating each primitive with simple geometric cases (head-on hit,
104- miss, ray from inside).
105-
106-Specific to this implementation (not in the books, which are C++/Java):
107-
108-- the Go `Object` interface with `Group` as a generic combinator
109-- parallel rendering in row chunks across goroutines (`sync.WaitGroup`)
110-- the hand-rolled PNG encoder (`png.go`) — the books only write PPM
111-- inverse-square light falloff with clamping in `shade`
112-
113-If you want to go further, *Ray Tracing in One Weekend* (and its sequel,
114-*Realistic Rendering with Path Tracing*) is the reference to read — this
115-codebase closely follows its progression.
116-
117-## Limitations
118-
119-No textures, no area lights (emissive surfaces are the light source for
120-the path tracer), no triangle meshes. Anti-aliasing is grid
121-supersampling (`Renderer.Samples`) plus stochastic sampling in the
122-`PathTracer`. Transforms are per-object (`Transform` wrapper); there is
123-no scene-wide model/view matrix yet.
124-
125-Note: `Vec3` and `Color` are intentionally written unkeyed
126-(`povray.Vec3{1, 0, 0}`) — `go vet` will flag this in example code; that's
127-expected for a math library.
deleted demos/qwen-style/workspace/ROADMAP.md +0 -62
deleted file mode 100644
@@ -1,62 +0,0 @@
1-# Roadmap — what's missing
2-
3-Status snapshot of the `povray` library and the work that remains, in
4-order of impact. The core is solid (primitives, materials, shadows,
5-reflections, refraction, parallel rendering, PNG, tests); this is a
6-*minimal* ray tracer with a clear path to grow.
7-
8-## 1. Rendering — biggest visual impact
9-
10-- **Transformations** (done — see `transform.go`). Objects can now be
11- translated, rotated and (non-uniformly) scaled via the `Transform`
12- wrapper; normals are mapped with the inverse transpose so lighting
13- stays correct under non-uniform scale.
14-- **Anti-aliasing** (done — see `Renderer.Samples`). Grid supersampling:
15- `Samples: n` shoots an n×n grid of rays per pixel and averages them.
16- All examples now render with `Samples: 2`.
17-- **Textures** — color by position (checker, gradient, noise). Today a
18- material is a single fixed color.
19-- **Normal mapping / bump** — non-smooth surfaces without meshes.
20-- **Path tracing** (done — see `pathtracer.go`). The `PathTracer`
21- bounces rays between surfaces: global illumination, soft shadows,
22- colored bounce light. Lights are emissive surfaces
23- (`LightMaterial`); renders are deterministic.
24-- **Area lights** — partially covered: emissive surfaces (boxes) act as
25- area lights in the path tracer. A dedicated, more efficient area-light
26- sampler (importance sampling over the light) is still missing.
27-- **More primitives**: torus, cylinder, cone, and especially a
28- **triangle mesh** (to import `.obj` models).
29-
30-## 2. Physical quality
31-
32-- **Gamma / tone mapping** — the current output is linear; gamma 2.2
33- makes colors closer to what the eye expects.
34-- **Refraction and shadows** — transparent objects are ignored by
35- shadow rays (documented limitation); acceptable, but noted.
36-
37-## 3. Engineering — what a publishable package needs
38-
39-- **Real module path** — `github.com/user/povray` is a placeholder;
40- nobody can `go get` the package.
41-- **CI** — no `.github/workflows`; a simple `go test` + `go vet` +
42- `gofmt -l` on GitHub Actions.
43-- **Test coverage** — examples have no tests; missing cases: `Box` seen
44- from inside, nested `Group`, refraction through the glass sphere,
45- `SavePNG` on a rendered (not 4×3) image.
46-- **`go vet` clean everywhere** — unkeyed `Vec3{...}`/`Color{...}`
47- literals in examples are still flagged; key them, add a `//nolint`,
48- or provide constructors.
49-- **License** — no `LICENSE` (MIT/Apache), so the code is "all rights
50- reserved" by default.
51-- **`Makefile`** — `test`, `render`, `clean` targets.
52-- **Docs** — public types are commented, but there is no package-level
53- doc (`// Package povray ...`).
54-
55-## Suggested order
56-
57-1. ~~**Transformations**~~ — done (`transform.go`, `examples/transform`).
58-2. ~~**Anti-aliasing**~~ — done (`Renderer.Samples`, grid supersampling).
59-3. ~~**Path tracing**~~ — done (`pathtracer.go`, `examples/pathtrace`).
60-
61-Package hygiene (real module path + `LICENSE` + CI + `Makefile`) is
62-about an hour of work and makes the package publishable.
deleted file mode 100644
@@ -1,62 +0,0 @@
1-# Roadmap — what's missing
2-
3-Status snapshot of the `povray` library and the work that remains, in
4-order of impact. The core is solid (primitives, materials, shadows,
5-reflections, refraction, parallel rendering, PNG, tests); this is a
6-*minimal* ray tracer with a clear path to grow.
7-
8-## 1. Rendering — biggest visual impact
9-
10-- **Transformations** (done — see `transform.go`). Objects can now be
11- translated, rotated and (non-uniformly) scaled via the `Transform`
12- wrapper; normals are mapped with the inverse transpose so lighting
13- stays correct under non-uniform scale.
14-- **Anti-aliasing** (done — see `Renderer.Samples`). Grid supersampling:
15- `Samples: n` shoots an n×n grid of rays per pixel and averages them.
16- All examples now render with `Samples: 2`.
17-- **Textures** — color by position (checker, gradient, noise). Today a
18- material is a single fixed color.
19-- **Normal mapping / bump** — non-smooth surfaces without meshes.
20-- **Path tracing** (done — see `pathtracer.go`). The `PathTracer`
21- bounces rays between surfaces: global illumination, soft shadows,
22- colored bounce light. Lights are emissive surfaces
23- (`LightMaterial`); renders are deterministic.
24-- **Area lights** — partially covered: emissive surfaces (boxes) act as
25- area lights in the path tracer. A dedicated, more efficient area-light
26- sampler (importance sampling over the light) is still missing.
27-- **More primitives**: torus, cylinder, cone, and especially a
28- **triangle mesh** (to import `.obj` models).
29-
30-## 2. Physical quality
31-
32-- **Gamma / tone mapping** — the current output is linear; gamma 2.2
33- makes colors closer to what the eye expects.
34-- **Refraction and shadows** — transparent objects are ignored by
35- shadow rays (documented limitation); acceptable, but noted.
36-
37-## 3. Engineering — what a publishable package needs
38-
39-- **Real module path** — `github.com/user/povray` is a placeholder;
40- nobody can `go get` the package.
41-- **CI** — no `.github/workflows`; a simple `go test` + `go vet` +
42- `gofmt -l` on GitHub Actions.
43-- **Test coverage** — examples have no tests; missing cases: `Box` seen
44- from inside, nested `Group`, refraction through the glass sphere,
45- `SavePNG` on a rendered (not 4×3) image.
46-- **`go vet` clean everywhere** — unkeyed `Vec3{...}`/`Color{...}`
47- literals in examples are still flagged; key them, add a `//nolint`,
48- or provide constructors.
49-- **License** — no `LICENSE` (MIT/Apache), so the code is "all rights
50- reserved" by default.
51-- **`Makefile`** — `test`, `render`, `clean` targets.
52-- **Docs** — public types are commented, but there is no package-level
53- doc (`// Package povray ...`).
54-
55-## Suggested order
56-
57-1. ~~**Transformations**~~ — done (`transform.go`, `examples/transform`).
58-2. ~~**Anti-aliasing**~~ — done (`Renderer.Samples`, grid supersampling).
59-3. ~~**Path tracing**~~ — done (`pathtracer.go`, `examples/pathtrace`).
60-
61-Package hygiene (real module path + `LICENSE` + CI + `Makefile`) is
62-about an hour of work and makes the package publishable.
deleted demos/qwen-style/workspace/camera.go +0 -47
deleted file mode 100644
@@ -1,47 +0,0 @@
1-package povray
2-
3-import "math"
4-
5-// Camera is a pinhole camera looking from Position toward LookAt.
6-type Camera struct {
7- Position Vec3
8- LookAt Vec3
9- Up Vec3
10- // Fov is the vertical field of view in degrees.
11- Fov float64
12-}
13-
14-// NewCamera makes a camera looking at the origin from a position,
15-// with the world Y axis as up and a 60 degree vertical field of view.
16-func NewCamera(pos Vec3) Camera {
17- return Camera{Position: pos, LookAt: Vec3{}, Up: Vec3{0, 1, 0}, Fov: 60}
18-}
19-
20-// Basis returns the camera's right, up and forward unit vectors.
21-func (c Camera) Basis() (right, up, fwd Vec3) {
22- fwd = c.LookAt.Sub(c.Position).Norm()
23- right = fwd.Cross(c.Up).Norm()
24- up = right.Cross(fwd)
25- return
26-}
27-
28-// RayAt returns the ray through the center of pixel (px, py).
29-func (c Camera) RayAt(px, py, w, h int) Ray {
30- return c.RayAtUV(float64(px)+0.5, float64(py)+0.5, w, h)
31-}
32-
33-// RayAtUV returns the ray through the point (u, v) in pixel units
34-// (u in [0, w), v in [0, h)); used for supersampling.
35-func (c Camera) RayAtUV(u, v float64, w, h int) Ray {
36- right, up, fwd := c.Basis()
37- // Vertical half-angle; horizontal scaled by aspect.
38- tanHalf := math.Tan(radians(c.Fov) / 2)
39- du := 2 * tanHalf
40- dv := du * float64(w) / float64(h)
41- uu := (2*u/float64(w) - 1) * dv
42- vv := (1 - 2*v/float64(h)) * du
43- dir := fwd.Add(right.Scale(uu)).Add(up.Scale(vv)).Norm()
44- return Ray{Origin: c.Position, Dir: dir}
45-}
46-
47-func radians(deg float64) float64 { return deg * math.Pi / 180 }
deleted file mode 100644
@@ -1,47 +0,0 @@
1-package povray
2-
3-import "math"
4-
5-// Camera is a pinhole camera looking from Position toward LookAt.
6-type Camera struct {
7- Position Vec3
8- LookAt Vec3
9- Up Vec3
10- // Fov is the vertical field of view in degrees.
11- Fov float64
12-}
13-
14-// NewCamera makes a camera looking at the origin from a position,
15-// with the world Y axis as up and a 60 degree vertical field of view.
16-func NewCamera(pos Vec3) Camera {
17- return Camera{Position: pos, LookAt: Vec3{}, Up: Vec3{0, 1, 0}, Fov: 60}
18-}
19-
20-// Basis returns the camera's right, up and forward unit vectors.
21-func (c Camera) Basis() (right, up, fwd Vec3) {
22- fwd = c.LookAt.Sub(c.Position).Norm()
23- right = fwd.Cross(c.Up).Norm()
24- up = right.Cross(fwd)
25- return
26-}
27-
28-// RayAt returns the ray through the center of pixel (px, py).
29-func (c Camera) RayAt(px, py, w, h int) Ray {
30- return c.RayAtUV(float64(px)+0.5, float64(py)+0.5, w, h)
31-}
32-
33-// RayAtUV returns the ray through the point (u, v) in pixel units
34-// (u in [0, w), v in [0, h)); used for supersampling.
35-func (c Camera) RayAtUV(u, v float64, w, h int) Ray {
36- right, up, fwd := c.Basis()
37- // Vertical half-angle; horizontal scaled by aspect.
38- tanHalf := math.Tan(radians(c.Fov) / 2)
39- du := 2 * tanHalf
40- dv := du * float64(w) / float64(h)
41- uu := (2*u/float64(w) - 1) * dv
42- vv := (1 - 2*v/float64(h)) * du
43- dir := fwd.Add(right.Scale(uu)).Add(up.Scale(vv)).Norm()
44- return Ray{Origin: c.Position, Dir: dir}
45-}
46-
47-func radians(deg float64) float64 { return deg * math.Pi / 180 }
deleted demos/qwen-style/workspace/docs/TUTORIEL.md +0 -302
deleted file mode 100644
@@ -1,302 +0,0 @@
1-# Tutoriel — créer de belles images
2-
3-Ce guide vous apprend à composer des scènes avec la bibliothèque
4-`povray`, du plus simple au plus avancé. Chaque niveau s'appuie sur le
5-précédent ; chaque code est complet et rendable tel quel
6-(`go run` dans le dossier du projet).
7-
8-Les exemples finis sont dans `examples/` — quand un niveau vous semble
9-flou, lisez le fichier correspondant.
10-
11-## Niveau 0 — Votre première image
12-
13-Une scène, une sphère, une lumière, une caméra. C'est tout.
14-
15-```go
16-package main
17-
18-import (
19- "log"
20-
21- "github.com/user/povray"
22-)
23-
24-func main() {
25- scene := povray.NewScene()
26- scene.AddObject(
27- povray.Sphere{Center: povray.Vec3{0, 1, 0}, Radius: 1},
28- povray.Material{Diffuse: povray.Red, Specular: povray.White, Shininess: 30},
29- )
30- scene.AddObject(povray.NewPlaneXY(0),
31- povray.Material{Diffuse: povray.Gray, Ambient: 0.3})
32- scene.Lights = []povray.Light{povray.NewLight(povray.Vec3{4, 8, 6}, povray.White)}
33-
34- cam := povray.NewCamera(povray.Vec3{0, 2, 8})
35- r := povray.Renderer{Scene: scene, Width: 800, Height: 600, Samples: 2}
36- if err := r.Render(cam).SavePNG("hello.png"); err != nil {
37- log.Fatal(err)
38- }
39-}
40-```
41-
42-Ce qu'il faut retenir :
43-
44-- **`NewScene()`** crée une scène (fond blanc, gamma 2.2).
45-- **`AddObject(obj, mat)`** ajoute un objet *et* son matériau.
46-- **`Lights`** est une liste de lumières ponctuelles.
47-- **`Renderer`** : `Width`/`Height` en pixels, `Samples: 2` = 4 rayons
48- par pixel (anti-aliasing). Sans `Samples`, les bords sont en dents de
49- scie.
50-- **`SavePNG`** écrit le fichier.
51-
52-## Niveau 1 — Matériaux et couleurs
53-
54-Le matériau est ce qui fait l'image. Les champs utiles :
55-
56-| Champ | Effet | Valeurs typiques |
57-|---|---|---|
58-| `Diffuse` | couleur de base | `povray.Red`, `povray.Color{0.8, 0.2, 0.1}` |
59-| `Specular` | couleur du reflet | `povray.White` |
60-| `Shininess` | taille du reflet (plus = plus petit) | 10 (mat) à 100 (chrome) |
61-| `Ambient` | lumière résiduelle dans l'ombre | 0.1 à 0.4 |
62-| `Reflectivity` | force du miroir | 0 à 1 |
63-| `Transparency` | force du verre | 0 à 1 |
64-| `IOR` | indice de réfraction du verre | 1.5 (verre), 1.33 (eau) |
65-
66-**Règle d'or des couleurs** : les valeurs de `Diffuse` sont *linéaires*.
67-Un gris `0.5` rend plus sombre qu'il n'en a l'air (le gamma 2.2
68-l'éclaircit au rendu, mais pas autant que le direct). Pour des couleurs
69-"vives", montez les valeurs : `Color{0.9, 0.1, 0.1}` est plus rouge que
70-`Color{0.5, 0.05, 0.05}`.
71-
72-Deux matériaux opposés sur des objets voisins créent du contraste —
73-c'est la base de toute belle image :
74-
75-```go
76-// Un sol sombre porte des objets clairs (et inversement).
77-scene.AddObject(povray.NewPlaneXY(0),
78- povray.Material{Diffuse: povray.Color{0.12, 0.12, 0.15}, Ambient: 0.3})
79-
80-scene.AddObject(povray.Sphere{Center: povray.Vec3{-2, 1, 0}, Radius: 1},
81- povray.Material{Diffuse: povray.Color{0.9, 0.3, 0.2}, Specular: povray.White, Shininess: 40})
82-scene.AddObject(povray.Sphere{Center: povray.Vec3{2, 1, 0}, Radius: 1},
83- povray.Material{Diffuse: povray.Color{0.2, 0.7, 0.9}, Specular: povray.White, Shininess: 40})
84-```
85-
86-Voir `examples/spheres/` pour une scène complète.
87-
88-## Niveau 2 — La caméra et la composition
89-
90-La caméra est un trou d'épingle : elle est à `Position`, regarde
91-`LookAt`, avec `Fov` (champ vertical en degrés).
92-
93-```go
94-cam := povray.NewCamera(povray.Vec3{0, 3, 10}) // reculer et monter
95-cam.LookAt = povray.Vec3{0, 1, -2} // regarder plus loin que l'objet
96-cam.Fov = 50 // < 60 = téléobjectif, > 70 = grand angle
97-```
98-
99-**Principes de composition** :
100-
101-1. **Éloignez la caméra** plutôt que de zoomer : un Fov large
102- (grand angle) déforme les objets proches ; un Fov étroit les
103- comprime et aplatit la scène.
104-2. **Regardez légèrement au-dessus ou en dessous** du sujet, pas
105- exactement au milieu : `LookAt` un peu plus haut que le centre des
106- objets donne une vue "de dessus" naturelle.
107-3. **Laissez de la place** : le sujet au centre exact est rarement
108- joli. Décalez-le d'un tiers (règle des tiers) en déplaçant
109- `LookAt` dans la direction opposée.
110-4. **Le plan du sol doit sortir du cadre** : si l'horizon est visible,
111- la scène a l'air petite. Montez la caméra ou rapprochez le sol.
112-
113-## Niveau 3 — Transformations : placer les objets
114-
115-Les objets bruts vivent dans l'espace monde. Pour les incliner,
116-étirer ou les disposer en boucle, on les enveloppe dans un
117-`Transform` :
118-
119-```go
120-// Une boîte inclinée de 30° autour de X, déplacée à (-3, 0, -2).
121-tilted := povray.Transform{
122- Object: povray.Box{Min: povray.Vec3{-1, 0, -1}, Max: povray.Vec3{1, 2, 1}},
123- Matrix: povray.Translate(-3, 0, -2).Multiply(povray.RotateX(math.Pi / 6)),
124-}
125-scene.AddObject(tilted, mat)
126-```
127-
128-**L'ordre compte** : `Multiply` s'applique de droite à gauche.
129-`Translate(...).Multiply(RotateX(...))` = *d'abord* la rotation,
130-*puis* la translation. Pour "se déplacer puis tourner sur place",
131-inversez : `RotateX(...).Multiply(Translate(...))`.
132-
133-Les primitives de base :
134-
135-- `Translate(x, y, z)` — déplacer
136-- `RotateX/Y/Z(angle)` — tourner (radians : `math.Pi/2` = 90°)
137-- `Scale(x, y, z)` — étirer (une sphère échelonnée = ellipsoïde)
138-- `Identity()` — rien
139-
140-**Disposer en boucle** est le moyen le plus simple de créer des
141-scènes riches :
142-
143-```go
144-var ring []povray.Object
145-for i := 0; i < 12; i++ {
146- a := 2 * math.Pi * float64(i) / 12
147- m := povray.RotateY(a).Multiply(povray.Translate(0, 0, -5.5))
148- ring = append(ring, povray.Transform{
149- Object: povray.Box{Min: povray.Vec3{-0.4, 0, -0.4}, Max: povray.Vec3{0.4, 1.6, 0.4}},
150- Matrix: m,
151- })
152-}
153-scene.AddObjects(ring, povray.Material{Diffuse: povray.Gold, ...})
154-```
155-
156-Voir `examples/transform/` (anneau de boîtes, ellipsoïde miroir) et
157-`examples/procedural/` (grille de 200 sphères dont la couleur varie
158-par position).
159-
160-## Niveau 4 — Miroirs et verre
161-
162-Deux champs de matériau suffisent :
163-
164-```go
165-// Miroir : Reflectivity élevé, Diffuse sombre.
166-mirror := povray.Material{
167- Diffuse: povray.Color{0.1, 0.1, 0.12},
168- Reflectivity: 0.9,
169- Specular: povray.White, Shininess: 80,
170-}
171-
172-// Verre : Transparency élevé + IOR.
173-glass := povray.Material{
174- Diffuse: povray.White,
175- Transparency: 0.95,
176- IOR: 1.5,
177- Specular: povray.White, Shininess: 100,
178- Ambient: 0.05,
179-}
180-```
181-
182-**Astuces** :
183-
184-- Un miroir pur (`Reflectivity: 1`, `Diffuse: Black`) est joli mais
185- plat. Un `Reflectivity: 0.5` avec un `Diffuse` coloré donne un
186- "métal teinté" très photogénique.
187-- Le verre est plus beau quand il *contient* ou *cache* quelque chose
188- de coloré. Une sphère de verre seule est presque invisible.
189-- L'effet "sol miroir" : un plan avec `Reflectivity: 0.3` et
190- `Diffuse` sombre fait des reflets doux sous les objets.
191-
192-Voir `examples/mirror/`.
193-
194-## Niveau 5 — Path tracing : lumière réaliste
195-
196-Le `Renderer` classique éclaire les surfaces une à une. Le
197-**`PathTracer`** fait rebondir la lumière de surface en surface :
198-éclairage indirect (une sphère rouge teintée le sol autour d'elle),
199-ombres douces, lumière de couleur. C'est ce qui donne des images
200-"réelles".
201-
202-```go
203-// Les lumières du path tracer sont des SURFACES ÉMISSIVES.
204-panel := povray.Transform{
205- Object: povray.Box{Min: povray.Vec3{-1.5, 0, -1.5}, Max: povray.Vec3{1.5, 0.15, 1.5}},
206- Matrix: povray.Translate(0, 9, -2),
207-}
208-scene.AddObject(panel, povray.LightMaterial(povray.Color{1, 0.85, 0.65}, 14))
209-
210-tracer := povray.PathTracer{Scene: scene, Width: 800, Height: 600, Samples: 64}
211-img := tracer.Render(cam)
212-```
213-
214-Différences avec le `Renderer` :
215-
216-- **Les `Scene.Lights` sont ignorées** — la lumière vient des matériaux
217- avec `Emission` (utilisez `LightMaterial(couleur, intensité)`).
218-- **`Samples` = nombre de rayons par pixel**, pas une grille. 16 =
219- rapide (un peu de bruit), 64 = propre, 256 = publication.
220-- **Le bruit est normal** : c'est un estimateur aléatoire. Plus de
221- samples = moins de bruit. Le rendu est déterministe (même image à
222- chaque fois pour la même config).
223-- **Les intensités** : un panneau de lumière a besoin d'une intensité
224- de 10 à 20 pour bien éclairer une scène à cette échelle.
225-
226-Voir `examples/pathtrace/` : une pièce avec panneau chaud + bandeau
227-froid (la lumière rebondit et teinte les murs), sphère rouge, miroir
228-bleu, verre.
229-
230-## Niveau 6 — Recettes pour de belles images
231-
232-### La palette
233-
234-Choisissez 2 à 3 couleurs de base et un neutre. Les complémentaires
235-se font face :
236-
237-```go
238-var (
239- warm = povray.Color{0.9, 0.45, 0.2} // orange
240- cool = povray.Color{0.2, 0.4, 0.85} // bleu
241- neutral = povray.Color{0.25, 0.25, 0.28} // gris foncé pour le sol
242-)
243-```
244-
245-Un sol neutre et sombre est le fond le plus polyvalent : il ne
246-concurrence pas les objets.
247-
248-### La lumière
249-
250-- **Deux lumières** valent mieux qu'une : une clé (forte, chaude,
251- en hauteur) + une rembourrage (faible, froide, en face) :
252-
253-```go
254-scene.Lights = []povray.Light{
255- povray.NewLight(povray.Vec3{6, 10, 8}, povray.Color{1, 0.95, 0.85}),
256- povray.NewLight(povray.Vec3{-8, 4, -4}, povray.Color{0.5, 0.6, 1.0}),
257-}
258-```
259-
260-- Placez la lumière **en hauteur et de côté** : en face, les ombres
261- disparaissent derrière les objets ; en haut, tout est plat.
262-- Dans le path tracer, la **couleur de la lumière** est plus
263- importante que son intensité : un panneau `Color{1, 0.85, 0.65}`
264- (chaud) + un bandeau `Color{0.4, 0.6, 1}` (froid) crée une ambiance
265- immédiatement.
266-
267-### La profondeur
268-
269-- **Tailles variées** : un grand + un petit objet donnent l'échelle.
270-- **Écartez en Z** : les objets à des distances différentes créent de
271- la profondeur ; à la même Z, la scène est plate.
272-- **Un élément au premier plan** (partiellement hors cadre) donne un
273- effet "regard à travers".
274-
275-### Les réglages finaux
276-
277-| Réglage | Pour… |
278-|---|---|
279-| `Samples: 2` (Renderer) | aperçu rapide, propre |
280-| `Samples: 4` (Renderer) | rendu final du Renderer |
281-| `Samples: 16` (PathTracer) | itération (bruit visible) |
282-| `Samples: 64` (PathTracer) | rendu final path traced |
283-| `Width: 1920, Height: 1080` | image grande taille |
284-| `Fov: 45` | "photo" compressée, élégante |
285-| `Fov: 75` | vue immersive, scène large |
286-
287-### Ce qui fait une image "finie"
288-
289-1. Le sujet occupe environ **un tiers** du cadre.
290-2. Il y a **du contraste** : au moins une zone claire et une sombre.
291-3. Les **couleurs s'équilibrent** : pas de zone qui crie plus fort que
292- le sujet.
293-4. Le **sol a une teinte** (jamais du blanc pur, jamais du noir pur).
294-5. On a regardé le rendu **à taille réduite** : si l'image fonctionne
295- en 200 px de large, elle fonctionne en grand.
296-
297-## Où aller ensuite
298-
299-- `examples/` — cinq scènes complètes à décompiler.
300-- `README.md` — référence de l'API (anglais).
301-- `ROADMAP.md` — ce que la bibliothèque ne sait pas encore faire
302- (textures, maillages `.obj`, area lights…).
deleted file mode 100644
@@ -1,302 +0,0 @@
1-# Tutoriel — créer de belles images
2-
3-Ce guide vous apprend à composer des scènes avec la bibliothèque
4-`povray`, du plus simple au plus avancé. Chaque niveau s'appuie sur le
5-précédent ; chaque code est complet et rendable tel quel
6-(`go run` dans le dossier du projet).
7-
8-Les exemples finis sont dans `examples/` — quand un niveau vous semble
9-flou, lisez le fichier correspondant.
10-
11-## Niveau 0 — Votre première image
12-
13-Une scène, une sphère, une lumière, une caméra. C'est tout.
14-
15-```go
16-package main
17-
18-import (
19- "log"
20-
21- "github.com/user/povray"
22-)
23-
24-func main() {
25- scene := povray.NewScene()
26- scene.AddObject(
27- povray.Sphere{Center: povray.Vec3{0, 1, 0}, Radius: 1},
28- povray.Material{Diffuse: povray.Red, Specular: povray.White, Shininess: 30},
29- )
30- scene.AddObject(povray.NewPlaneXY(0),
31- povray.Material{Diffuse: povray.Gray, Ambient: 0.3})
32- scene.Lights = []povray.Light{povray.NewLight(povray.Vec3{4, 8, 6}, povray.White)}
33-
34- cam := povray.NewCamera(povray.Vec3{0, 2, 8})
35- r := povray.Renderer{Scene: scene, Width: 800, Height: 600, Samples: 2}
36- if err := r.Render(cam).SavePNG("hello.png"); err != nil {
37- log.Fatal(err)
38- }
39-}
40-```
41-
42-Ce qu'il faut retenir :
43-
44-- **`NewScene()`** crée une scène (fond blanc, gamma 2.2).
45-- **`AddObject(obj, mat)`** ajoute un objet *et* son matériau.
46-- **`Lights`** est une liste de lumières ponctuelles.
47-- **`Renderer`** : `Width`/`Height` en pixels, `Samples: 2` = 4 rayons
48- par pixel (anti-aliasing). Sans `Samples`, les bords sont en dents de
49- scie.
50-- **`SavePNG`** écrit le fichier.
51-
52-## Niveau 1 — Matériaux et couleurs
53-
54-Le matériau est ce qui fait l'image. Les champs utiles :
55-
56-| Champ | Effet | Valeurs typiques |
57-|---|---|---|
58-| `Diffuse` | couleur de base | `povray.Red`, `povray.Color{0.8, 0.2, 0.1}` |
59-| `Specular` | couleur du reflet | `povray.White` |
60-| `Shininess` | taille du reflet (plus = plus petit) | 10 (mat) à 100 (chrome) |
61-| `Ambient` | lumière résiduelle dans l'ombre | 0.1 à 0.4 |
62-| `Reflectivity` | force du miroir | 0 à 1 |
63-| `Transparency` | force du verre | 0 à 1 |
64-| `IOR` | indice de réfraction du verre | 1.5 (verre), 1.33 (eau) |
65-
66-**Règle d'or des couleurs** : les valeurs de `Diffuse` sont *linéaires*.
67-Un gris `0.5` rend plus sombre qu'il n'en a l'air (le gamma 2.2
68-l'éclaircit au rendu, mais pas autant que le direct). Pour des couleurs
69-"vives", montez les valeurs : `Color{0.9, 0.1, 0.1}` est plus rouge que
70-`Color{0.5, 0.05, 0.05}`.
71-
72-Deux matériaux opposés sur des objets voisins créent du contraste —
73-c'est la base de toute belle image :
74-
75-```go
76-// Un sol sombre porte des objets clairs (et inversement).
77-scene.AddObject(povray.NewPlaneXY(0),
78- povray.Material{Diffuse: povray.Color{0.12, 0.12, 0.15}, Ambient: 0.3})
79-
80-scene.AddObject(povray.Sphere{Center: povray.Vec3{-2, 1, 0}, Radius: 1},
81- povray.Material{Diffuse: povray.Color{0.9, 0.3, 0.2}, Specular: povray.White, Shininess: 40})
82-scene.AddObject(povray.Sphere{Center: povray.Vec3{2, 1, 0}, Radius: 1},
83- povray.Material{Diffuse: povray.Color{0.2, 0.7, 0.9}, Specular: povray.White, Shininess: 40})
84-```
85-
86-Voir `examples/spheres/` pour une scène complète.
87-
88-## Niveau 2 — La caméra et la composition
89-
90-La caméra est un trou d'épingle : elle est à `Position`, regarde
91-`LookAt`, avec `Fov` (champ vertical en degrés).
92-
93-```go
94-cam := povray.NewCamera(povray.Vec3{0, 3, 10}) // reculer et monter
95-cam.LookAt = povray.Vec3{0, 1, -2} // regarder plus loin que l'objet
96-cam.Fov = 50 // < 60 = téléobjectif, > 70 = grand angle
97-```
98-
99-**Principes de composition** :
100-
101-1. **Éloignez la caméra** plutôt que de zoomer : un Fov large
102- (grand angle) déforme les objets proches ; un Fov étroit les
103- comprime et aplatit la scène.
104-2. **Regardez légèrement au-dessus ou en dessous** du sujet, pas
105- exactement au milieu : `LookAt` un peu plus haut que le centre des
106- objets donne une vue "de dessus" naturelle.
107-3. **Laissez de la place** : le sujet au centre exact est rarement
108- joli. Décalez-le d'un tiers (règle des tiers) en déplaçant
109- `LookAt` dans la direction opposée.
110-4. **Le plan du sol doit sortir du cadre** : si l'horizon est visible,
111- la scène a l'air petite. Montez la caméra ou rapprochez le sol.
112-
113-## Niveau 3 — Transformations : placer les objets
114-
115-Les objets bruts vivent dans l'espace monde. Pour les incliner,
116-étirer ou les disposer en boucle, on les enveloppe dans un
117-`Transform` :
118-
119-```go
120-// Une boîte inclinée de 30° autour de X, déplacée à (-3, 0, -2).
121-tilted := povray.Transform{
122- Object: povray.Box{Min: povray.Vec3{-1, 0, -1}, Max: povray.Vec3{1, 2, 1}},
123- Matrix: povray.Translate(-3, 0, -2).Multiply(povray.RotateX(math.Pi / 6)),
124-}
125-scene.AddObject(tilted, mat)
126-```
127-
128-**L'ordre compte** : `Multiply` s'applique de droite à gauche.
129-`Translate(...).Multiply(RotateX(...))` = *d'abord* la rotation,
130-*puis* la translation. Pour "se déplacer puis tourner sur place",
131-inversez : `RotateX(...).Multiply(Translate(...))`.
132-
133-Les primitives de base :
134-
135-- `Translate(x, y, z)` — déplacer
136-- `RotateX/Y/Z(angle)` — tourner (radians : `math.Pi/2` = 90°)
137-- `Scale(x, y, z)` — étirer (une sphère échelonnée = ellipsoïde)
138-- `Identity()` — rien
139-
140-**Disposer en boucle** est le moyen le plus simple de créer des
141-scènes riches :
142-
143-```go
144-var ring []povray.Object
145-for i := 0; i < 12; i++ {
146- a := 2 * math.Pi * float64(i) / 12
147- m := povray.RotateY(a).Multiply(povray.Translate(0, 0, -5.5))
148- ring = append(ring, povray.Transform{
149- Object: povray.Box{Min: povray.Vec3{-0.4, 0, -0.4}, Max: povray.Vec3{0.4, 1.6, 0.4}},
150- Matrix: m,
151- })
152-}
153-scene.AddObjects(ring, povray.Material{Diffuse: povray.Gold, ...})
154-```
155-
156-Voir `examples/transform/` (anneau de boîtes, ellipsoïde miroir) et
157-`examples/procedural/` (grille de 200 sphères dont la couleur varie
158-par position).
159-
160-## Niveau 4 — Miroirs et verre
161-
162-Deux champs de matériau suffisent :
163-
164-```go
165-// Miroir : Reflectivity élevé, Diffuse sombre.
166-mirror := povray.Material{
167- Diffuse: povray.Color{0.1, 0.1, 0.12},
168- Reflectivity: 0.9,
169- Specular: povray.White, Shininess: 80,
170-}
171-
172-// Verre : Transparency élevé + IOR.
173-glass := povray.Material{
174- Diffuse: povray.White,
175- Transparency: 0.95,
176- IOR: 1.5,
177- Specular: povray.White, Shininess: 100,
178- Ambient: 0.05,
179-}
180-```
181-
182-**Astuces** :
183-
184-- Un miroir pur (`Reflectivity: 1`, `Diffuse: Black`) est joli mais
185- plat. Un `Reflectivity: 0.5` avec un `Diffuse` coloré donne un
186- "métal teinté" très photogénique.
187-- Le verre est plus beau quand il *contient* ou *cache* quelque chose
188- de coloré. Une sphère de verre seule est presque invisible.
189-- L'effet "sol miroir" : un plan avec `Reflectivity: 0.3` et
190- `Diffuse` sombre fait des reflets doux sous les objets.
191-
192-Voir `examples/mirror/`.
193-
194-## Niveau 5 — Path tracing : lumière réaliste
195-
196-Le `Renderer` classique éclaire les surfaces une à une. Le
197-**`PathTracer`** fait rebondir la lumière de surface en surface :
198-éclairage indirect (une sphère rouge teintée le sol autour d'elle),
199-ombres douces, lumière de couleur. C'est ce qui donne des images
200-"réelles".
201-
202-```go
203-// Les lumières du path tracer sont des SURFACES ÉMISSIVES.
204-panel := povray.Transform{
205- Object: povray.Box{Min: povray.Vec3{-1.5, 0, -1.5}, Max: povray.Vec3{1.5, 0.15, 1.5}},
206- Matrix: povray.Translate(0, 9, -2),
207-}
208-scene.AddObject(panel, povray.LightMaterial(povray.Color{1, 0.85, 0.65}, 14))
209-
210-tracer := povray.PathTracer{Scene: scene, Width: 800, Height: 600, Samples: 64}
211-img := tracer.Render(cam)
212-```
213-
214-Différences avec le `Renderer` :
215-
216-- **Les `Scene.Lights` sont ignorées** — la lumière vient des matériaux
217- avec `Emission` (utilisez `LightMaterial(couleur, intensité)`).
218-- **`Samples` = nombre de rayons par pixel**, pas une grille. 16 =
219- rapide (un peu de bruit), 64 = propre, 256 = publication.
220-- **Le bruit est normal** : c'est un estimateur aléatoire. Plus de
221- samples = moins de bruit. Le rendu est déterministe (même image à
222- chaque fois pour la même config).
223-- **Les intensités** : un panneau de lumière a besoin d'une intensité
224- de 10 à 20 pour bien éclairer une scène à cette échelle.
225-
226-Voir `examples/pathtrace/` : une pièce avec panneau chaud + bandeau
227-froid (la lumière rebondit et teinte les murs), sphère rouge, miroir
228-bleu, verre.
229-
230-## Niveau 6 — Recettes pour de belles images
231-
232-### La palette
233-
234-Choisissez 2 à 3 couleurs de base et un neutre. Les complémentaires
235-se font face :
236-
237-```go
238-var (
239- warm = povray.Color{0.9, 0.45, 0.2} // orange
240- cool = povray.Color{0.2, 0.4, 0.85} // bleu
241- neutral = povray.Color{0.25, 0.25, 0.28} // gris foncé pour le sol
242-)
243-```
244-
245-Un sol neutre et sombre est le fond le plus polyvalent : il ne
246-concurrence pas les objets.
247-
248-### La lumière
249-
250-- **Deux lumières** valent mieux qu'une : une clé (forte, chaude,
251- en hauteur) + une rembourrage (faible, froide, en face) :
252-
253-```go
254-scene.Lights = []povray.Light{
255- povray.NewLight(povray.Vec3{6, 10, 8}, povray.Color{1, 0.95, 0.85}),
256- povray.NewLight(povray.Vec3{-8, 4, -4}, povray.Color{0.5, 0.6, 1.0}),
257-}
258-```
259-
260-- Placez la lumière **en hauteur et de côté** : en face, les ombres
261- disparaissent derrière les objets ; en haut, tout est plat.
262-- Dans le path tracer, la **couleur de la lumière** est plus
263- importante que son intensité : un panneau `Color{1, 0.85, 0.65}`
264- (chaud) + un bandeau `Color{0.4, 0.6, 1}` (froid) crée une ambiance
265- immédiatement.
266-
267-### La profondeur
268-
269-- **Tailles variées** : un grand + un petit objet donnent l'échelle.
270-- **Écartez en Z** : les objets à des distances différentes créent de
271- la profondeur ; à la même Z, la scène est plate.
272-- **Un élément au premier plan** (partiellement hors cadre) donne un
273- effet "regard à travers".
274-
275-### Les réglages finaux
276-
277-| Réglage | Pour… |
278-|---|---|
279-| `Samples: 2` (Renderer) | aperçu rapide, propre |
280-| `Samples: 4` (Renderer) | rendu final du Renderer |
281-| `Samples: 16` (PathTracer) | itération (bruit visible) |
282-| `Samples: 64` (PathTracer) | rendu final path traced |
283-| `Width: 1920, Height: 1080` | image grande taille |
284-| `Fov: 45` | "photo" compressée, élégante |
285-| `Fov: 75` | vue immersive, scène large |
286-
287-### Ce qui fait une image "finie"
288-
289-1. Le sujet occupe environ **un tiers** du cadre.
290-2. Il y a **du contraste** : au moins une zone claire et une sombre.
291-3. Les **couleurs s'équilibrent** : pas de zone qui crie plus fort que
292- le sujet.
293-4. Le **sol a une teinte** (jamais du blanc pur, jamais du noir pur).
294-5. On a regardé le rendu **à taille réduite** : si l'image fonctionne
295- en 200 px de large, elle fonctionne en grand.
296-
297-## Où aller ensuite
298-
299-- `examples/` — cinq scènes complètes à décompiler.
300-- `README.md` — référence de l'API (anglais).
301-- `ROADMAP.md` — ce que la bibliothèque ne sait pas encore faire
302- (textures, maillages `.obj`, area lights…).
deleted demos/qwen-style/workspace/examples/mirror/main.go +0 -86
deleted file mode 100644
@@ -1,86 +0,0 @@
1-// Command mirror shows reflections and refraction: a glass sphere on
2-// a mirror floor, with colored spheres behind it.
3-package main
4-
5-import (
6- "fmt"
7- "log"
8-
9- "github.com/user/povray"
10-)
11-
12-func main() {
13- scene := povray.NewScene()
14- scene.Background = povray.Color{0.05, 0.05, 0.1}
15- scene.MaxDepth = 10
16-
17- // Mirror floor.
18- floorMat := povray.Material{
19- Diffuse: povray.Color{0.2, 0.2, 0.25},
20- Specular: povray.White,
21- Shininess: 100,
22- Ambient: 0.1,
23- Reflectivity: 0.7,
24- }
25- scene.AddObject(povray.NewPlaneXY(0), floorMat)
26-
27- // Glass sphere in front.
28- glass := povray.Material{
29- Diffuse: povray.Color{0.9, 0.95, 1},
30- Specular: povray.White,
31- Shininess: 120,
32- Ambient: 0.05,
33- Reflectivity: 0.2,
34- Transparency: 0.9,
35- IOR: 1.5,
36- }
37- scene.AddObject(povray.Sphere{Center: povray.Vec3{0, 1, 0}, Radius: 1}, glass)
38-
39- // Colored spheres behind the glass.
40- behind := []struct {
41- x float64
42- c povray.Color
43- }{
44- {-3, povray.Red},
45- {0, povray.Yellow},
46- {3, povray.Cyan},
47- }
48- for _, b := range behind {
49- mat := povray.Material{
50- Diffuse: b.c,
51- Specular: povray.White,
52- Shininess: 40,
53- Ambient: 0.15,
54- }
55- scene.AddObject(povray.Sphere{Center: povray.Vec3{b.x, 1.2, -5}, Radius: 1.2}, mat)
56- }
57-
58- // A gold, mirror-like box.
59- gold := povray.Material{
60- Diffuse: povray.Gold,
61- Specular: povray.White,
62- Shininess: 80,
63- Ambient: 0.15,
64- Reflectivity: 0.5,
65- }
66- scene.AddObject(povray.Box{
67- Min: povray.Vec3{2.2, 0, -2.5},
68- Max: povray.Vec3{3.4, 1.2, -1.3},
69- }, gold)
70-
71- scene.Lights = []povray.Light{
72- povray.NewLight(povray.Vec3{0, 10, 4}, povray.White),
73- povray.NewLight(povray.Vec3{-5, 3, 8}, povray.Color{0.7, 0.8, 1}),
74- povray.NewLight(povray.Vec3{5, 3, 8}, povray.Color{1, 0.85, 0.7}),
75- }
76-
77- cam := povray.NewCamera(povray.Vec3{0, 2, 8})
78- cam.Fov = 55
79-
80- renderer := povray.Renderer{Scene: scene, Width: 800, Height: 600, Samples: 2}
81- img := renderer.Render(cam)
82- if err := img.SavePNG("mirror.png"); err != nil {
83- log.Fatal(err)
84- }
85- fmt.Println("wrote mirror.png")
86-}
deleted file mode 100644
@@ -1,86 +0,0 @@
1-// Command mirror shows reflections and refraction: a glass sphere on
2-// a mirror floor, with colored spheres behind it.
3-package main
4-
5-import (
6- "fmt"
7- "log"
8-
9- "github.com/user/povray"
10-)
11-
12-func main() {
13- scene := povray.NewScene()
14- scene.Background = povray.Color{0.05, 0.05, 0.1}
15- scene.MaxDepth = 10
16-
17- // Mirror floor.
18- floorMat := povray.Material{
19- Diffuse: povray.Color{0.2, 0.2, 0.25},
20- Specular: povray.White,
21- Shininess: 100,
22- Ambient: 0.1,
23- Reflectivity: 0.7,
24- }
25- scene.AddObject(povray.NewPlaneXY(0), floorMat)
26-
27- // Glass sphere in front.
28- glass := povray.Material{
29- Diffuse: povray.Color{0.9, 0.95, 1},
30- Specular: povray.White,
31- Shininess: 120,
32- Ambient: 0.05,
33- Reflectivity: 0.2,
34- Transparency: 0.9,
35- IOR: 1.5,
36- }
37- scene.AddObject(povray.Sphere{Center: povray.Vec3{0, 1, 0}, Radius: 1}, glass)
38-
39- // Colored spheres behind the glass.
40- behind := []struct {
41- x float64
42- c povray.Color
43- }{
44- {-3, povray.Red},
45- {0, povray.Yellow},
46- {3, povray.Cyan},
47- }
48- for _, b := range behind {
49- mat := povray.Material{
50- Diffuse: b.c,
51- Specular: povray.White,
52- Shininess: 40,
53- Ambient: 0.15,
54- }
55- scene.AddObject(povray.Sphere{Center: povray.Vec3{b.x, 1.2, -5}, Radius: 1.2}, mat)
56- }
57-
58- // A gold, mirror-like box.
59- gold := povray.Material{
60- Diffuse: povray.Gold,
61- Specular: povray.White,
62- Shininess: 80,
63- Ambient: 0.15,
64- Reflectivity: 0.5,
65- }
66- scene.AddObject(povray.Box{
67- Min: povray.Vec3{2.2, 0, -2.5},
68- Max: povray.Vec3{3.4, 1.2, -1.3},
69- }, gold)
70-
71- scene.Lights = []povray.Light{
72- povray.NewLight(povray.Vec3{0, 10, 4}, povray.White),
73- povray.NewLight(povray.Vec3{-5, 3, 8}, povray.Color{0.7, 0.8, 1}),
74- povray.NewLight(povray.Vec3{5, 3, 8}, povray.Color{1, 0.85, 0.7}),
75- }
76-
77- cam := povray.NewCamera(povray.Vec3{0, 2, 8})
78- cam.Fov = 55
79-
80- renderer := povray.Renderer{Scene: scene, Width: 800, Height: 600, Samples: 2}
81- img := renderer.Render(cam)
82- if err := img.SavePNG("mirror.png"); err != nil {
83- log.Fatal(err)
84- }
85- fmt.Println("wrote mirror.png")
86-}
deleted demos/qwen-style/workspace/examples/pathtrace/main.go +0 -67
deleted file mode 100644
@@ -1,67 +0,0 @@
1-// Command pathtrace renders a scene with the path tracer: light is
2-// carried by rays bouncing between surfaces, which gives global
3-// illumination, soft shadows and colored bounce light.
4-package main
5-
6-import (
7- "fmt"
8- "log"
9- "math"
10-
11- "github.com/user/povray"
12-)
13-
14-func main() {
15- scene := povray.NewScene()
16- scene.Background = povray.Color{0.02, 0.03, 0.05}
17-
18- // A large room: floor and two walls, slightly tinted so the bounce
19- // light picks up color.
20- room := povray.Material{Diffuse: povray.Color{0.75, 0.72, 0.68}}
21- scene.AddObject(povray.NewPlaneXY(0), room)
22- scene.AddObject(povray.Box{Min: povray.Vec3{-12, 0, -12}, Max: povray.Vec3{12, 12, -11.9}},
23- povray.Material{Diffuse: povray.Color{0.55, 0.6, 0.75}})
24-
25- // A warm light panel hanging above the scene.
26- panel := povray.Transform{
27- Object: povray.Box{Min: povray.Vec3{-1.5, 0, -1.5}, Max: povray.Vec3{1.5, 0.15, 1.5}},
28- Matrix: povray.Translate(0, 9, -2),
29- }
30- scene.AddObject(panel, povray.LightMaterial(povray.Color{1, 0.85, 0.65}, 14))
31-
32- // A cool light strip along the back wall, for colored bounce.
33- strip := povray.Transform{
34- Object: povray.Box{Min: povray.Vec3{-4, 0, -0.2}, Max: povray.Vec3{4, 2.5, 0}},
35- Matrix: povray.Translate(0, 0, -11.8),
36- }
37- scene.AddObject(strip, povray.LightMaterial(povray.Color{0.4, 0.6, 1}, 6))
38-
39- // The subjects: a red sphere, a blue mirror, a glass sphere.
40- scene.AddObject(povray.Sphere{Center: povray.Vec3{-2.5, 1, -2}, Radius: 1},
41- povray.Material{Diffuse: povray.Color{0.85, 0.15, 0.15}})
42- scene.AddObject(povray.Sphere{Center: povray.Vec3{2.5, 1, -2}, Radius: 1},
43- povray.Material{Diffuse: povray.Color{0.1, 0.2, 0.8}, Reflectivity: 0.6})
44- scene.AddObject(povray.Sphere{Center: povray.Vec3{0, 1, 0}, Radius: 1},
45- povray.Material{Diffuse: povray.White, Transparency: 0.95, IOR: 1.5})
46-
47- // A few boxes, tilted, to catch the light.
48- for i := 0; i < 3; i++ {
49- b := povray.Transform{
50- Object: povray.Box{Min: povray.Vec3{-0.5, 0, -0.5}, Max: povray.Vec3{0.5, 1.5, 0.5}},
51- Matrix: povray.Translate(float64(i-1)*-4, 0, 2).
52- Multiply(povray.RotateY(math.Pi / 8 * float64(i))),
53- }
54- scene.AddObject(b, povray.Material{Diffuse: povray.Color{0.8, 0.6, 0.3}})
55- }
56-
57- cam := povray.NewCamera(povray.Vec3{0, 3, 10})
58- cam.LookAt = povray.Vec3{0, 1.5, -2}
59- cam.Fov = 50
60-
61- tracer := povray.PathTracer{Scene: scene, Width: 800, Height: 600, Samples: 64}
62- img := tracer.Render(cam)
63- if err := img.SavePNG("pathtrace.png"); err != nil {
64- log.Fatal(err)
65- }
66- fmt.Println("wrote pathtrace.png")
67-}
deleted file mode 100644
@@ -1,67 +0,0 @@
1-// Command pathtrace renders a scene with the path tracer: light is
2-// carried by rays bouncing between surfaces, which gives global
3-// illumination, soft shadows and colored bounce light.
4-package main
5-
6-import (
7- "fmt"
8- "log"
9- "math"
10-
11- "github.com/user/povray"
12-)
13-
14-func main() {
15- scene := povray.NewScene()
16- scene.Background = povray.Color{0.02, 0.03, 0.05}
17-
18- // A large room: floor and two walls, slightly tinted so the bounce
19- // light picks up color.
20- room := povray.Material{Diffuse: povray.Color{0.75, 0.72, 0.68}}
21- scene.AddObject(povray.NewPlaneXY(0), room)
22- scene.AddObject(povray.Box{Min: povray.Vec3{-12, 0, -12}, Max: povray.Vec3{12, 12, -11.9}},
23- povray.Material{Diffuse: povray.Color{0.55, 0.6, 0.75}})
24-
25- // A warm light panel hanging above the scene.
26- panel := povray.Transform{
27- Object: povray.Box{Min: povray.Vec3{-1.5, 0, -1.5}, Max: povray.Vec3{1.5, 0.15, 1.5}},
28- Matrix: povray.Translate(0, 9, -2),
29- }
30- scene.AddObject(panel, povray.LightMaterial(povray.Color{1, 0.85, 0.65}, 14))
31-
32- // A cool light strip along the back wall, for colored bounce.
33- strip := povray.Transform{
34- Object: povray.Box{Min: povray.Vec3{-4, 0, -0.2}, Max: povray.Vec3{4, 2.5, 0}},
35- Matrix: povray.Translate(0, 0, -11.8),
36- }
37- scene.AddObject(strip, povray.LightMaterial(povray.Color{0.4, 0.6, 1}, 6))
38-
39- // The subjects: a red sphere, a blue mirror, a glass sphere.
40- scene.AddObject(povray.Sphere{Center: povray.Vec3{-2.5, 1, -2}, Radius: 1},
41- povray.Material{Diffuse: povray.Color{0.85, 0.15, 0.15}})
42- scene.AddObject(povray.Sphere{Center: povray.Vec3{2.5, 1, -2}, Radius: 1},
43- povray.Material{Diffuse: povray.Color{0.1, 0.2, 0.8}, Reflectivity: 0.6})
44- scene.AddObject(povray.Sphere{Center: povray.Vec3{0, 1, 0}, Radius: 1},
45- povray.Material{Diffuse: povray.White, Transparency: 0.95, IOR: 1.5})
46-
47- // A few boxes, tilted, to catch the light.
48- for i := 0; i < 3; i++ {
49- b := povray.Transform{
50- Object: povray.Box{Min: povray.Vec3{-0.5, 0, -0.5}, Max: povray.Vec3{0.5, 1.5, 0.5}},
51- Matrix: povray.Translate(float64(i-1)*-4, 0, 2).
52- Multiply(povray.RotateY(math.Pi / 8 * float64(i))),
53- }
54- scene.AddObject(b, povray.Material{Diffuse: povray.Color{0.8, 0.6, 0.3}})
55- }
56-
57- cam := povray.NewCamera(povray.Vec3{0, 3, 10})
58- cam.LookAt = povray.Vec3{0, 1.5, -2}
59- cam.Fov = 50
60-
61- tracer := povray.PathTracer{Scene: scene, Width: 800, Height: 600, Samples: 64}
62- img := tracer.Render(cam)
63- if err := img.SavePNG("pathtrace.png"); err != nil {
64- log.Fatal(err)
65- }
66- fmt.Println("wrote pathtrace.png")
67-}
deleted demos/qwen-style/workspace/examples/procedural/main.go +0 -115
deleted file mode 100644
@@ -1,115 +0,0 @@
1-// Command procedural shows that a scene is just data: a grid of
2-// spheres whose colors and heights are computed in a loop, plus a
3-// ring of boxes, all generated from a few parameters.
4-package main
5-
6-import (
7- "fmt"
8- "log"
9- "math"
10-
11- "github.com/user/povray"
12-)
13-
14-func main() {
15- scene := povray.NewScene()
16- scene.Background = povray.Color{0.02, 0.03, 0.08}
17-
18- // Ground.
19- groundMat := povray.Material{
20- Diffuse: povray.Color{0.3, 0.32, 0.38},
21- Specular: povray.Color{0.2, 0.2, 0.2},
22- Shininess: 15,
23- Ambient: 0.25,
24- }
25- scene.AddObject(povray.NewPlaneXY(0), groundMat)
26-
27- // A 6x6 grid of spheres; color and height vary with position.
28- const n = 6
29- step := 1.6
30- for i := 0; i < n; i++ {
31- for j := 0; j < n; j++ {
32- x := (float64(i) - float64(n-1)/2) * step
33- z := (float64(j) - float64(n-1)/2) * step
34- dist := math.Hypot(x, z)
35- r := 0.55 - 0.1*math.Min(1, dist/6)
36- h := r + 0.25*math.Sin(dist*0.8)
37-
38- // Hue sweep across the grid.
39- t := float64(i+j) / float64(2*n)
40- c := hsv(0.6-0.5*t, 0.7, 0.9)
41-
42- mat := povray.Material{
43- Diffuse: c,
44- Specular: povray.White,
45- Shininess: 50,
46- Ambient: 0.1,
47- }
48- scene.AddObject(povray.Sphere{Center: povray.Vec3{x, h, z}, Radius: r}, mat)
49- }
50- }
51-
52- // A ring of boxes around the grid.
53- const m = 12
54- for k := 0; k < m; k++ {
55- a := 2 * math.Pi * float64(k) / m
56- x, z := 7.5*math.Cos(a), 7.5*math.Sin(a)
57- h := 1 + 0.8*math.Sin(3*a)
58- c := hsv(0.55+0.08*math.Sin(2*a), 0.5, 0.8)
59- mat := povray.Material{
60- Diffuse: c,
61- Specular: povray.White,
62- Shininess: 25,
63- Ambient: 0.15,
64- }
65- scene.AddObject(povray.Box{
66- Min: povray.Vec3{x - 0.4, 0, z - 0.4},
67- Max: povray.Vec3{x + 0.4, h, z + 0.4},
68- }, mat)
69- }
70-
71- // Three colored lights for a dramatic look.
72- scene.Lights = []povray.Light{
73- povray.NewLight(povray.Vec3{0, 12, 0}, povray.White),
74- povray.NewLight(povray.Vec3{10, 5, 10}, povray.Color{1, 0.4, 0.3}),
75- povray.NewLight(povray.Vec3{-10, 5, -10}, povray.Color{0.3, 0.4, 1}),
76- }
77-
78- cam := povray.NewCamera(povray.Vec3{0, 9, 16})
79- cam.LookAt = povray.Vec3{0, 0.5, 0}
80- cam.Fov = 50
81-
82- renderer := povray.Renderer{Scene: scene, Width: 800, Height: 600, Samples: 2}
83- img := renderer.Render(cam)
84- if err := img.SavePNG("procedural.png"); err != nil {
85- log.Fatal(err)
86- }
87- fmt.Println("wrote procedural.png")
88-}
89-
90-// hsv converts hue [0,1), saturation and value [0,1] to RGB.
91-func hsv(h, s, v float64) povray.Color {
92- h = math.Mod(h, 1)
93- if h < 0 {
94- h += 1
95- }
96- i := int(h * 6)
97- f := h*6 - float64(i)
98- p := v * (1 - s)
99- q := v * (1 - s*f)
100- t := v * (1 - s*(1-f))
101- switch i % 6 {
102- case 0:
103- return povray.Color{v, t, p}
104- case 1:
105- return povray.Color{q, v, p}
106- case 2:
107- return povray.Color{p, v, t}
108- case 3:
109- return povray.Color{p, q, v}
110- case 4:
111- return povray.Color{t, p, v}
112- default:
113- return povray.Color{v, p, q}
114- }
115-}
deleted file mode 100644
@@ -1,115 +0,0 @@
1-// Command procedural shows that a scene is just data: a grid of
2-// spheres whose colors and heights are computed in a loop, plus a
3-// ring of boxes, all generated from a few parameters.
4-package main
5-
6-import (
7- "fmt"
8- "log"
9- "math"
10-
11- "github.com/user/povray"
12-)
13-
14-func main() {
15- scene := povray.NewScene()
16- scene.Background = povray.Color{0.02, 0.03, 0.08}
17-
18- // Ground.
19- groundMat := povray.Material{
20- Diffuse: povray.Color{0.3, 0.32, 0.38},
21- Specular: povray.Color{0.2, 0.2, 0.2},
22- Shininess: 15,
23- Ambient: 0.25,
24- }
25- scene.AddObject(povray.NewPlaneXY(0), groundMat)
26-
27- // A 6x6 grid of spheres; color and height vary with position.
28- const n = 6
29- step := 1.6
30- for i := 0; i < n; i++ {
31- for j := 0; j < n; j++ {
32- x := (float64(i) - float64(n-1)/2) * step
33- z := (float64(j) - float64(n-1)/2) * step
34- dist := math.Hypot(x, z)
35- r := 0.55 - 0.1*math.Min(1, dist/6)
36- h := r + 0.25*math.Sin(dist*0.8)
37-
38- // Hue sweep across the grid.
39- t := float64(i+j) / float64(2*n)
40- c := hsv(0.6-0.5*t, 0.7, 0.9)
41-
42- mat := povray.Material{
43- Diffuse: c,
44- Specular: povray.White,
45- Shininess: 50,
46- Ambient: 0.1,
47- }
48- scene.AddObject(povray.Sphere{Center: povray.Vec3{x, h, z}, Radius: r}, mat)
49- }
50- }
51-
52- // A ring of boxes around the grid.
53- const m = 12
54- for k := 0; k < m; k++ {
55- a := 2 * math.Pi * float64(k) / m
56- x, z := 7.5*math.Cos(a), 7.5*math.Sin(a)
57- h := 1 + 0.8*math.Sin(3*a)
58- c := hsv(0.55+0.08*math.Sin(2*a), 0.5, 0.8)
59- mat := povray.Material{
60- Diffuse: c,
61- Specular: povray.White,
62- Shininess: 25,
63- Ambient: 0.15,
64- }
65- scene.AddObject(povray.Box{
66- Min: povray.Vec3{x - 0.4, 0, z - 0.4},
67- Max: povray.Vec3{x + 0.4, h, z + 0.4},
68- }, mat)
69- }
70-
71- // Three colored lights for a dramatic look.
72- scene.Lights = []povray.Light{
73- povray.NewLight(povray.Vec3{0, 12, 0}, povray.White),
74- povray.NewLight(povray.Vec3{10, 5, 10}, povray.Color{1, 0.4, 0.3}),
75- povray.NewLight(povray.Vec3{-10, 5, -10}, povray.Color{0.3, 0.4, 1}),
76- }
77-
78- cam := povray.NewCamera(povray.Vec3{0, 9, 16})
79- cam.LookAt = povray.Vec3{0, 0.5, 0}
80- cam.Fov = 50
81-
82- renderer := povray.Renderer{Scene: scene, Width: 800, Height: 600, Samples: 2}
83- img := renderer.Render(cam)
84- if err := img.SavePNG("procedural.png"); err != nil {
85- log.Fatal(err)
86- }
87- fmt.Println("wrote procedural.png")
88-}
89-
90-// hsv converts hue [0,1), saturation and value [0,1] to RGB.
91-func hsv(h, s, v float64) povray.Color {
92- h = math.Mod(h, 1)
93- if h < 0 {
94- h += 1
95- }
96- i := int(h * 6)
97- f := h*6 - float64(i)
98- p := v * (1 - s)
99- q := v * (1 - s*f)
100- t := v * (1 - s*(1-f))
101- switch i % 6 {
102- case 0:
103- return povray.Color{v, t, p}
104- case 1:
105- return povray.Color{q, v, p}
106- case 2:
107- return povray.Color{p, v, t}
108- case 3:
109- return povray.Color{p, q, v}
110- case 4:
111- return povray.Color{t, p, v}
112- default:
113- return povray.Color{v, p, q}
114- }
115-}
deleted demos/qwen-style/workspace/examples/spheres/main.go +0 -73
deleted file mode 100644
@@ -1,73 +0,0 @@
1-// Command spheres is the classic "first render" scene: a row of
2-// colored spheres and a box on a floor, lit by two point lights.
3-package main
4-
5-import (
6- "fmt"
7- "log"
8-
9- "github.com/user/povray"
10-)
11-
12-func main() {
13- scene := povray.NewScene()
14- scene.Background = povray.Color{0.9, 0.9, 0.95}
15-
16- // Floor.
17- floor := povray.NewPlaneXY(0)
18- floorMat := povray.Material{
19- Diffuse: povray.Color{0.75, 0.75, 0.8},
20- Specular: povray.Color{0.3, 0.3, 0.3},
21- Shininess: 20,
22- Ambient: 0.35,
23- }
24- scene.AddObject(floor, floorMat)
25-
26- // A row of colored spheres.
27- spheres := []struct {
28- x float64
29- c povray.Color
30- }{
31- {-2.2, povray.Red},
32- {-0.8, povray.Green},
33- {0.6, povray.Blue},
34- {2.0, povray.Gold},
35- }
36- for _, s := range spheres {
37- mat := povray.Material{
38- Diffuse: s.c,
39- Specular: povray.White,
40- Shininess: 60,
41- Ambient: 0.1,
42- }
43- scene.AddObject(povray.Sphere{Center: povray.Vec3{s.x, 1, 0}, Radius: 1}, mat)
44- }
45-
46- // A box for variety.
47- boxMat := povray.Material{
48- Diffuse: povray.Purple,
49- Specular: povray.White,
50- Shininess: 30,
51- Ambient: 0.15,
52- }
53- scene.AddObject(povray.Box{
54- Min: povray.Vec3{-0.9, 0, -3.5},
55- Max: povray.Vec3{0.9, 1.8, -1.7},
56- }, boxMat)
57-
58- // Lights: a warm key light and a cool fill light.
59- scene.Lights = []povray.Light{
60- povray.NewLight(povray.Vec3{4, 8, 6}, povray.Color{1, 0.95, 0.85}),
61- povray.NewLight(povray.Vec3{-6, 4, -4}, povray.Color{0.6, 0.7, 0.9}),
62- }
63-
64- cam := povray.NewCamera(povray.Vec3{0, 2.5, 10})
65- cam.Fov = 55
66-
67- renderer := povray.Renderer{Scene: scene, Width: 800, Height: 600, Samples: 2}
68- img := renderer.Render(cam)
69- if err := img.SavePNG("spheres.png"); err != nil {
70- log.Fatal(err)
71- }
72- fmt.Println("wrote spheres.png")
73-}
deleted file mode 100644
@@ -1,73 +0,0 @@
1-// Command spheres is the classic "first render" scene: a row of
2-// colored spheres and a box on a floor, lit by two point lights.
3-package main
4-
5-import (
6- "fmt"
7- "log"
8-
9- "github.com/user/povray"
10-)
11-
12-func main() {
13- scene := povray.NewScene()
14- scene.Background = povray.Color{0.9, 0.9, 0.95}
15-
16- // Floor.
17- floor := povray.NewPlaneXY(0)
18- floorMat := povray.Material{
19- Diffuse: povray.Color{0.75, 0.75, 0.8},
20- Specular: povray.Color{0.3, 0.3, 0.3},
21- Shininess: 20,
22- Ambient: 0.35,
23- }
24- scene.AddObject(floor, floorMat)
25-
26- // A row of colored spheres.
27- spheres := []struct {
28- x float64
29- c povray.Color
30- }{
31- {-2.2, povray.Red},
32- {-0.8, povray.Green},
33- {0.6, povray.Blue},
34- {2.0, povray.Gold},
35- }
36- for _, s := range spheres {
37- mat := povray.Material{
38- Diffuse: s.c,
39- Specular: povray.White,
40- Shininess: 60,
41- Ambient: 0.1,
42- }
43- scene.AddObject(povray.Sphere{Center: povray.Vec3{s.x, 1, 0}, Radius: 1}, mat)
44- }
45-
46- // A box for variety.
47- boxMat := povray.Material{
48- Diffuse: povray.Purple,
49- Specular: povray.White,
50- Shininess: 30,
51- Ambient: 0.15,
52- }
53- scene.AddObject(povray.Box{
54- Min: povray.Vec3{-0.9, 0, -3.5},
55- Max: povray.Vec3{0.9, 1.8, -1.7},
56- }, boxMat)
57-
58- // Lights: a warm key light and a cool fill light.
59- scene.Lights = []povray.Light{
60- povray.NewLight(povray.Vec3{4, 8, 6}, povray.Color{1, 0.95, 0.85}),
61- povray.NewLight(povray.Vec3{-6, 4, -4}, povray.Color{0.6, 0.7, 0.9}),
62- }
63-
64- cam := povray.NewCamera(povray.Vec3{0, 2.5, 10})
65- cam.Fov = 55
66-
67- renderer := povray.Renderer{Scene: scene, Width: 800, Height: 600, Samples: 2}
68- img := renderer.Render(cam)
69- if err := img.SavePNG("spheres.png"); err != nil {
70- log.Fatal(err)
71- }
72- fmt.Println("wrote spheres.png")
73-}
deleted demos/qwen-style/workspace/examples/transform/main.go +0 -77
deleted file mode 100644
@@ -1,77 +0,0 @@
1-// Command transform renders a scene built entirely from transformed
2-// primitives: tilted boxes, a stretched ellipsoid and a ring of rotated
3-// boxes, to show that objects can live in any orientation.
4-package main
5-
6-import (
7- "fmt"
8- "log"
9- "math"
10-
11- "github.com/user/povray"
12-)
13-
14-func main() {
15- scene := povray.NewScene()
16- scene.MaxDepth = 4
17- scene.Lights = []povray.Light{
18- povray.NewLight(povray.Vec3{6, 10, 8}, povray.White),
19- povray.NewLight(povray.Vec3{-8, 4, -4}, povray.Color{0.5, 0.6, 1.0}),
20- }
21-
22- ground := povray.Material{Diffuse: povray.Color{0.18, 0.18, 0.22}, Ambient: 0.35}
23- scene.AddObject(povray.NewPlaneXY(0), ground)
24-
25- // A box tilted 45° around X, leaning on the ground.
26- tilted := povray.Transform{
27- Object: povray.Box{Min: povray.Vec3{-1, 0, -1}, Max: povray.Vec3{1, 2, 1}},
28- Matrix: povray.Translate(-3, 0, -2).Multiply(povray.RotateX(math.Pi / 5)),
29- }
30- scene.AddObject(tilted, povray.Material{
31- Diffuse: povray.Color{0.9, 0.3, 0.2}, Specular: povray.White,
32- Shininess: 40, Ambient: 0.1,
33- })
34-
35- // A stretched ellipsoid (scaled sphere), mirror-finished.
36- ellipsoid := povray.Transform{
37- Object: povray.Sphere{Center: povray.Vec3{0, 0, 0}, Radius: 1},
38- Matrix: povray.Translate(3, 1.2, -3).Multiply(povray.Scale(1.6, 1, 1)).Multiply(povray.RotateY(math.Pi / 6)),
39- }
40- scene.AddObject(ellipsoid, povray.Material{
41- Diffuse: povray.Color{0.2, 0.7, 0.9}, Specular: povray.White,
42- Shininess: 80, Ambient: 0.1, Reflectivity: 0.5,
43- })
44-
45- // A ring of 12 boxes, each rotated around Y and tilted around X.
46- ring := make([]povray.Object, 0, 12)
47- for i := 0; i < 12; i++ {
48- a := 2 * math.Pi * float64(i) / 12
49- m := povray.Translate(0, 0, 0).
50- Multiply(povray.RotateY(a)).
51- Multiply(povray.Translate(0, 0, -5.5)).
52- Multiply(povray.RotateX(math.Pi / 4))
53- ring = append(ring, povray.Transform{
54- Object: povray.Box{Min: povray.Vec3{-0.4, 0, -0.4}, Max: povray.Vec3{0.4, 1.6, 0.4}},
55- Matrix: m,
56- })
57- }
58- scene.AddObject(povray.Group{Objects: ring}, povray.Material{
59- Diffuse: povray.Color{0.85, 0.75, 0.3}, Specular: povray.White,
60- Shininess: 30, Ambient: 0.15,
61- })
62-
63- // A glass sphere for refraction, sitting on the ground.
64- scene.AddObject(povray.Sphere{Center: povray.Vec3{0, 1, 1}, Radius: 1},
65- povray.Material{Diffuse: povray.White, Transparency: 0.9, IOR: 1.5,
66- Shininess: 100, Specular: povray.White, Ambient: 0.05})
67-
68- cam := povray.NewCamera(povray.Vec3{0, 3.5, 12})
69- cam.LookAt = povray.Vec3{0, 1, -2}
70-
71- renderer := povray.Renderer{Scene: scene, Width: 800, Height: 600, Samples: 2}
72- img := renderer.Render(cam)
73- if err := img.SavePNG("transform.png"); err != nil {
74- log.Fatal(err)
75- }
76- fmt.Println("wrote transform.png")
77-}
deleted file mode 100644
@@ -1,77 +0,0 @@
1-// Command transform renders a scene built entirely from transformed
2-// primitives: tilted boxes, a stretched ellipsoid and a ring of rotated
3-// boxes, to show that objects can live in any orientation.
4-package main
5-
6-import (
7- "fmt"
8- "log"
9- "math"
10-
11- "github.com/user/povray"
12-)
13-
14-func main() {
15- scene := povray.NewScene()
16- scene.MaxDepth = 4
17- scene.Lights = []povray.Light{
18- povray.NewLight(povray.Vec3{6, 10, 8}, povray.White),
19- povray.NewLight(povray.Vec3{-8, 4, -4}, povray.Color{0.5, 0.6, 1.0}),
20- }
21-
22- ground := povray.Material{Diffuse: povray.Color{0.18, 0.18, 0.22}, Ambient: 0.35}
23- scene.AddObject(povray.NewPlaneXY(0), ground)
24-
25- // A box tilted 45° around X, leaning on the ground.
26- tilted := povray.Transform{
27- Object: povray.Box{Min: povray.Vec3{-1, 0, -1}, Max: povray.Vec3{1, 2, 1}},
28- Matrix: povray.Translate(-3, 0, -2).Multiply(povray.RotateX(math.Pi / 5)),
29- }
30- scene.AddObject(tilted, povray.Material{
31- Diffuse: povray.Color{0.9, 0.3, 0.2}, Specular: povray.White,
32- Shininess: 40, Ambient: 0.1,
33- })
34-
35- // A stretched ellipsoid (scaled sphere), mirror-finished.
36- ellipsoid := povray.Transform{
37- Object: povray.Sphere{Center: povray.Vec3{0, 0, 0}, Radius: 1},
38- Matrix: povray.Translate(3, 1.2, -3).Multiply(povray.Scale(1.6, 1, 1)).Multiply(povray.RotateY(math.Pi / 6)),
39- }
40- scene.AddObject(ellipsoid, povray.Material{
41- Diffuse: povray.Color{0.2, 0.7, 0.9}, Specular: povray.White,
42- Shininess: 80, Ambient: 0.1, Reflectivity: 0.5,
43- })
44-
45- // A ring of 12 boxes, each rotated around Y and tilted around X.
46- ring := make([]povray.Object, 0, 12)
47- for i := 0; i < 12; i++ {
48- a := 2 * math.Pi * float64(i) / 12
49- m := povray.Translate(0, 0, 0).
50- Multiply(povray.RotateY(a)).
51- Multiply(povray.Translate(0, 0, -5.5)).
52- Multiply(povray.RotateX(math.Pi / 4))
53- ring = append(ring, povray.Transform{
54- Object: povray.Box{Min: povray.Vec3{-0.4, 0, -0.4}, Max: povray.Vec3{0.4, 1.6, 0.4}},
55- Matrix: m,
56- })
57- }
58- scene.AddObject(povray.Group{Objects: ring}, povray.Material{
59- Diffuse: povray.Color{0.85, 0.75, 0.3}, Specular: povray.White,
60- Shininess: 30, Ambient: 0.15,
61- })
62-
63- // A glass sphere for refraction, sitting on the ground.
64- scene.AddObject(povray.Sphere{Center: povray.Vec3{0, 1, 1}, Radius: 1},
65- povray.Material{Diffuse: povray.White, Transparency: 0.9, IOR: 1.5,
66- Shininess: 100, Specular: povray.White, Ambient: 0.05})
67-
68- cam := povray.NewCamera(povray.Vec3{0, 3.5, 12})
69- cam.LookAt = povray.Vec3{0, 1, -2}
70-
71- renderer := povray.Renderer{Scene: scene, Width: 800, Height: 600, Samples: 2}
72- img := renderer.Render(cam)
73- if err := img.SavePNG("transform.png"); err != nil {
74- log.Fatal(err)
75- }
76- fmt.Println("wrote transform.png")
77-}
deleted demos/qwen-style/workspace/go.mod +0 -3
deleted file mode 100644
@@ -1,3 +0,0 @@
1-module github.com/user/povray
2-
3-go 1.27.3
deleted file mode 100644
@@ -1,3 +0,0 @@
1-module github.com/user/povray
2-
3-go 1.27.3
deleted demos/qwen-style/workspace/material.go +0 -75
deleted file mode 100644
@@ -1,75 +0,0 @@
1-package povray
2-
3-// Color is an RGB value, any magnitude (values > 1 are clipped or
4-// tone-mapped at render time).
5-type Color struct {
6- R, G, B float64
7-}
8-
9-func (c Color) Add(o Color) Color { return Color{c.R + o.R, c.G + o.G, c.B + o.B} }
10-func (c Color) Scale(s float64) Color {
11- return Color{c.R * s, c.G * s, c.B * s}
12-}
13-func (c Color) Mul(o Color) Color { return Color{c.R * o.R, c.G * o.G, c.B * o.B} }
14-
15-// Common colors.
16-var (
17- White = Color{1, 1, 1}
18- Black = Color{0, 0, 0}
19- Red = Color{0.9, 0.1, 0.1}
20- Green = Color{0.1, 0.8, 0.2}
21- Blue = Color{0.15, 0.3, 0.9}
22- Yellow = Color{0.95, 0.85, 0.2}
23- Orange = Color{0.95, 0.5, 0.1}
24- Purple = Color{0.6, 0.2, 0.8}
25- Cyan = Color{0.1, 0.8, 0.9}
26- Gray = Color{0.5, 0.5, 0.5}
27- Silver = Color{0.85, 0.85, 0.88}
28- Gold = Color{1.0, 0.75, 0.25}
29-)
30-
31-// Material describes how a surface reacts to light.
32-type Material struct {
33- // Diffuse is the base surface color.
34- Diffuse Color
35- // Specular is the highlight color (White for a plain white glint).
36- Specular Color
37- // Shininess controls highlight size (higher = tighter).
38- Shininess float64
39- // Ambient is the fraction of diffuse kept with no light at all.
40- Ambient float64
41- // Reflectivity is the mirror strength, 0..1.
42- Reflectivity float64
43- // Transparency is the glass strength, 0..1 (1 = fully see-through).
44- Transparency float64
45- // IOR is the index of refraction for transparent surfaces.
46- IOR float64
47- // Emission is the light the surface itself radiates (used by the
48- // path tracer; see LightMaterial). Values above 1 make a brighter
49- // light.
50- Emission Color
51-}
52-
53-// DefaultMaterial is a plain white matte surface.
54-func DefaultMaterial() Material {
55- return Material{Diffuse: White, Specular: White, Shininess: 10, Ambient: 0.1}
56-}
57-
58-// LightMaterial makes an emissive surface — a light source for the path
59-// tracer. intensity scales the color (1 is a dim panel, 10 a bright one).
60-func LightMaterial(c Color, intensity float64) Material {
61- return Material{Emission: c.Scale(intensity), Specular: White, Shininess: 10}
62-}
63-
64-// Light is a point light.
65-type Light struct {
66- Color Color
67- Position Vec3
68- // Intensity scales the light (default 1).
69- Intensity float64
70-}
71-
72-// NewLight makes a light with full intensity.
73-func NewLight(pos Vec3, c Color) Light {
74- return Light{Color: c, Position: pos, Intensity: 1}
75-}
deleted file mode 100644
@@ -1,75 +0,0 @@
1-package povray
2-
3-// Color is an RGB value, any magnitude (values > 1 are clipped or
4-// tone-mapped at render time).
5-type Color struct {
6- R, G, B float64
7-}
8-
9-func (c Color) Add(o Color) Color { return Color{c.R + o.R, c.G + o.G, c.B + o.B} }
10-func (c Color) Scale(s float64) Color {
11- return Color{c.R * s, c.G * s, c.B * s}
12-}
13-func (c Color) Mul(o Color) Color { return Color{c.R * o.R, c.G * o.G, c.B * o.B} }
14-
15-// Common colors.
16-var (
17- White = Color{1, 1, 1}
18- Black = Color{0, 0, 0}
19- Red = Color{0.9, 0.1, 0.1}
20- Green = Color{0.1, 0.8, 0.2}
21- Blue = Color{0.15, 0.3, 0.9}
22- Yellow = Color{0.95, 0.85, 0.2}
23- Orange = Color{0.95, 0.5, 0.1}
24- Purple = Color{0.6, 0.2, 0.8}
25- Cyan = Color{0.1, 0.8, 0.9}
26- Gray = Color{0.5, 0.5, 0.5}
27- Silver = Color{0.85, 0.85, 0.88}
28- Gold = Color{1.0, 0.75, 0.25}
29-)
30-
31-// Material describes how a surface reacts to light.
32-type Material struct {
33- // Diffuse is the base surface color.
34- Diffuse Color
35- // Specular is the highlight color (White for a plain white glint).
36- Specular Color
37- // Shininess controls highlight size (higher = tighter).
38- Shininess float64
39- // Ambient is the fraction of diffuse kept with no light at all.
40- Ambient float64
41- // Reflectivity is the mirror strength, 0..1.
42- Reflectivity float64
43- // Transparency is the glass strength, 0..1 (1 = fully see-through).
44- Transparency float64
45- // IOR is the index of refraction for transparent surfaces.
46- IOR float64
47- // Emission is the light the surface itself radiates (used by the
48- // path tracer; see LightMaterial). Values above 1 make a brighter
49- // light.
50- Emission Color
51-}
52-
53-// DefaultMaterial is a plain white matte surface.
54-func DefaultMaterial() Material {
55- return Material{Diffuse: White, Specular: White, Shininess: 10, Ambient: 0.1}
56-}
57-
58-// LightMaterial makes an emissive surface — a light source for the path
59-// tracer. intensity scales the color (1 is a dim panel, 10 a bright one).
60-func LightMaterial(c Color, intensity float64) Material {
61- return Material{Emission: c.Scale(intensity), Specular: White, Shininess: 10}
62-}
63-
64-// Light is a point light.
65-type Light struct {
66- Color Color
67- Position Vec3
68- // Intensity scales the light (default 1).
69- Intensity float64
70-}
71-
72-// NewLight makes a light with full intensity.
73-func NewLight(pos Vec3, c Color) Light {
74- return Light{Color: c, Position: pos, Intensity: 1}
75-}
deleted demos/qwen-style/workspace/objects.go +0 -129
deleted file mode 100644
@@ -1,129 +0,0 @@
1-package povray
2-
3-import "math"
4-
5-// Object is anything a ray can intersect.
6-type Object interface {
7- Intersect(r Ray) (Hit, bool)
8-}
9-
10-// Sphere
11-type Sphere struct {
12- Center Vec3
13- Radius float64
14-}
15-
16-func (s Sphere) Intersect(r Ray) (Hit, bool) {
17- oc := r.Origin.Sub(s.Center)
18- b := 2 * oc.Dot(r.Dir)
19- c := oc.Dot(oc) - s.Radius*s.Radius
20- disc := b*b - 4*c
21- if disc < 0 {
22- return Hit{}, false
23- }
24- var t float64
25- sqrtDisc := math.Sqrt(disc)
26- t0 := (-b - sqrtDisc) / 2
27- t1 := (-b + sqrtDisc) / 2
28- const eps = 1e-4
29- switch {
30- case t0 > eps:
31- t = t0
32- case t1 > eps:
33- t = t1
34- default:
35- return Hit{}, false
36- }
37- p := r.Origin.Add(r.Dir.Scale(t))
38- return Hit{Point: p, Dist: t, Normal: p.Sub(s.Center).Norm()}, true
39-}
40-
41-// Plane is the infinite surface ax + by + cz + d = 0, with the normal
42-// pointing in the (a,b,c) direction.
43-type Plane struct {
44- Normal Vec3
45- D float64
46-}
47-
48-// NewPlaneXY makes the horizontal plane y = h.
49-func NewPlaneXY(h float64) Plane { return Plane{Normal: Vec3{0, 1, 0}, D: -h} }
50-
51-func (p Plane) Intersect(r Ray) (Hit, bool) {
52- denom := p.Normal.Dot(r.Dir)
53- if math.Abs(denom) < 1e-8 {
54- return Hit{}, false
55- }
56- t := -(p.Normal.Dot(r.Origin) + p.D) / denom
57- if t < 1e-4 {
58- return Hit{}, false
59- }
60- pt := r.Origin.Add(r.Dir.Scale(t))
61- n := p.Normal
62- if n.Dot(r.Dir) > 0 {
63- n = n.Neg()
64- }
65- return Hit{Point: pt, Dist: t, Normal: n}, true
66-}
67-
68-// Box is an axis-aligned box.
69-type Box struct {
70- Min, Max Vec3
71-}
72-
73-func (b Box) Intersect(r Ray) (Hit, bool) {
74- var tmin, tmax float64
75- tmin, tmax = -1e30, 1e30
76- var n Vec3
77- for i, o := range [3]float64{r.Origin.X, r.Origin.Y, r.Origin.Z} {
78- d := [3]float64{r.Dir.X, r.Dir.Y, r.Dir.Z}[i]
79- mn := [3]float64{b.Min.X, b.Min.Y, b.Min.Z}[i]
80- mx := [3]float64{b.Max.X, b.Max.Y, b.Max.Z}[i]
81- if math.Abs(d) < 1e-8 {
82- if o < mn || o > mx {
83- return Hit{}, false
84- }
85- continue
86- }
87- t0 := (mn - o) / d
88- t1 := (mx - o) / d
89- if t0 > t1 {
90- t0, t1 = t1, t0
91- }
92- if t0 > tmin {
93- tmin = t0
94- if i == 0 {
95- n = Vec3{-math.Copysign(1, d), 0, 0}
96- } else if i == 1 {
97- n = Vec3{0, -math.Copysign(1, d), 0}
98- } else {
99- n = Vec3{0, 0, -math.Copysign(1, d)}
100- }
101- }
102- if t1 < tmax {
103- tmax = t1
104- }
105- }
106- if tmax < math.Max(tmin, 1e-4) {
107- return Hit{}, false
108- }
109- t := tmin
110- if t < 1e-4 {
111- t = tmax
112- }
113- return Hit{Point: r.Origin.Add(r.Dir.Scale(t)), Dist: t, Normal: n}, true
114-}
115-
116-// Group intersects all children and keeps the closest hit.
117-type Group struct {
118- Objects []Object
119-}
120-
121-func (g Group) Intersect(r Ray) (Hit, bool) {
122- best, found := Hit{}, false
123- for _, o := range g.Objects {
124- if h, ok := o.Intersect(r); ok && (!found || h.Dist < best.Dist) {
125- best, found = h, true
126- }
127- }
128- return best, found
129-}
deleted file mode 100644
@@ -1,129 +0,0 @@
1-package povray
2-
3-import "math"
4-
5-// Object is anything a ray can intersect.
6-type Object interface {
7- Intersect(r Ray) (Hit, bool)
8-}
9-
10-// Sphere
11-type Sphere struct {
12- Center Vec3
13- Radius float64
14-}
15-
16-func (s Sphere) Intersect(r Ray) (Hit, bool) {
17- oc := r.Origin.Sub(s.Center)
18- b := 2 * oc.Dot(r.Dir)
19- c := oc.Dot(oc) - s.Radius*s.Radius
20- disc := b*b - 4*c
21- if disc < 0 {
22- return Hit{}, false
23- }
24- var t float64
25- sqrtDisc := math.Sqrt(disc)
26- t0 := (-b - sqrtDisc) / 2
27- t1 := (-b + sqrtDisc) / 2
28- const eps = 1e-4
29- switch {
30- case t0 > eps:
31- t = t0
32- case t1 > eps:
33- t = t1
34- default:
35- return Hit{}, false
36- }
37- p := r.Origin.Add(r.Dir.Scale(t))
38- return Hit{Point: p, Dist: t, Normal: p.Sub(s.Center).Norm()}, true
39-}
40-
41-// Plane is the infinite surface ax + by + cz + d = 0, with the normal
42-// pointing in the (a,b,c) direction.
43-type Plane struct {
44- Normal Vec3
45- D float64
46-}
47-
48-// NewPlaneXY makes the horizontal plane y = h.
49-func NewPlaneXY(h float64) Plane { return Plane{Normal: Vec3{0, 1, 0}, D: -h} }
50-
51-func (p Plane) Intersect(r Ray) (Hit, bool) {
52- denom := p.Normal.Dot(r.Dir)
53- if math.Abs(denom) < 1e-8 {
54- return Hit{}, false
55- }
56- t := -(p.Normal.Dot(r.Origin) + p.D) / denom
57- if t < 1e-4 {
58- return Hit{}, false
59- }
60- pt := r.Origin.Add(r.Dir.Scale(t))
61- n := p.Normal
62- if n.Dot(r.Dir) > 0 {
63- n = n.Neg()
64- }
65- return Hit{Point: pt, Dist: t, Normal: n}, true
66-}
67-
68-// Box is an axis-aligned box.
69-type Box struct {
70- Min, Max Vec3
71-}
72-
73-func (b Box) Intersect(r Ray) (Hit, bool) {
74- var tmin, tmax float64
75- tmin, tmax = -1e30, 1e30
76- var n Vec3
77- for i, o := range [3]float64{r.Origin.X, r.Origin.Y, r.Origin.Z} {
78- d := [3]float64{r.Dir.X, r.Dir.Y, r.Dir.Z}[i]
79- mn := [3]float64{b.Min.X, b.Min.Y, b.Min.Z}[i]
80- mx := [3]float64{b.Max.X, b.Max.Y, b.Max.Z}[i]
81- if math.Abs(d) < 1e-8 {
82- if o < mn || o > mx {
83- return Hit{}, false
84- }
85- continue
86- }
87- t0 := (mn - o) / d
88- t1 := (mx - o) / d
89- if t0 > t1 {
90- t0, t1 = t1, t0
91- }
92- if t0 > tmin {
93- tmin = t0
94- if i == 0 {
95- n = Vec3{-math.Copysign(1, d), 0, 0}
96- } else if i == 1 {
97- n = Vec3{0, -math.Copysign(1, d), 0}
98- } else {
99- n = Vec3{0, 0, -math.Copysign(1, d)}
100- }
101- }
102- if t1 < tmax {
103- tmax = t1
104- }
105- }
106- if tmax < math.Max(tmin, 1e-4) {
107- return Hit{}, false
108- }
109- t := tmin
110- if t < 1e-4 {
111- t = tmax
112- }
113- return Hit{Point: r.Origin.Add(r.Dir.Scale(t)), Dist: t, Normal: n}, true
114-}
115-
116-// Group intersects all children and keeps the closest hit.
117-type Group struct {
118- Objects []Object
119-}
120-
121-func (g Group) Intersect(r Ray) (Hit, bool) {
122- best, found := Hit{}, false
123- for _, o := range g.Objects {
124- if h, ok := o.Intersect(r); ok && (!found || h.Dist < best.Dist) {
125- best, found = h, true
126- }
127- }
128- return best, found
129-}
deleted demos/qwen-style/workspace/pathtracer.go +0 -148
deleted file mode 100644
@@ -1,148 +0,0 @@
1-package povray
2-
3-import (
4- "math"
5- "math/rand/v2"
6- "sync"
7-)
8-
9-// PathTracer renders a scene with path tracing: each ray bounces around
10-// the scene, scattering diffusely at matte surfaces, until it hits an
11-// emissive surface or leaves the scene. This gives global illumination
12-// and soft shadows, at the cost of a longer render (use Samples to
13-// control quality vs. time).
14-//
15-// Lights for path tracing are emissive surfaces: give an object a
16-// material with Emission set (see LightMaterial). Point lights in
17-// Scene.Lights are ignored by the path tracer.
18-type PathTracer struct {
19- Scene *Scene
20- // Width and Height of the image in pixels.
21- Width, Height int
22- // Samples is the number of primary rays per pixel.
23- Samples int
24- // Threads is the number of worker goroutines (0 = 4).
25- Threads int
26-}
27-
28-// Render renders the scene and returns the image. Each worker goroutine
29-// uses its own random stream, so a given configuration always produces
30-// the same image.
31-func (p *PathTracer) Render(cam Camera) *Image {
32- samples := p.Samples
33- if samples < 1 {
34- samples = 1
35- }
36- threads := p.Threads
37- if threads <= 0 {
38- threads = 4
39- }
40- img := NewImage(p.Width, p.Height)
41-
42- var wg sync.WaitGroup
43- rowsPer := (p.Height + threads - 1) / threads
44- for c := 0; c < threads; c++ {
45- y0, y1 := c*rowsPer, (c+1)*rowsPer
46- if y1 > p.Height {
47- y1 = p.Height
48- }
49- if y0 >= y1 {
50- break
51- }
52- wg.Add(1)
53- go func(y0, y1 int, rng *rand.Rand) {
54- defer wg.Done()
55- for y := y0; y < y1; y++ {
56- for x := 0; x < p.Width; x++ {
57- var col Color
58- for smp := 0; smp < samples; smp++ {
59- u := float64(x) + rng.Float64()
60- v := float64(y) + rng.Float64()
61- col = col.Add(p.trace(cam.RayAtUV(u, v, p.Width, p.Height), 0, rng))
62- }
63- img.Set(x, y, col.Scale(1.0/float64(samples)))
64- }
65- }
66- }(y0, y1, rand.New(rand.NewPCG(uint64(12345+c), uint64(987654321+c))))
67- }
68- wg.Wait()
69- return img
70-}
71-
72-// trace follows one path through the scene and returns the accumulated
73-// radiance.
74-func (p *PathTracer) trace(ray Ray, depth int, rng *rand.Rand) Color {
75- // Hard bounce limit: glass and mirrors can bounce a ray forever;
76- // past 50 bounces the remaining contribution is negligible.
77- if depth > 50 {
78- return Black
79- }
80- s := p.Scene
81- idx, hit := closestHit(s.Objects, ray)
82- if idx < 0 {
83- return s.Background
84- }
85- hit.Flip(ray)
86- mat := s.Materials[idx]
87-
88- // An emissive surface radiates its own light.
89- col := mat.Emission
90-
91- // Specular: mirror the ray and keep going.
92- if mat.Reflectivity > 0 {
93- r := ray.Dir.Sub(hit.Normal.Scale(2 * ray.Dir.Dot(hit.Normal))).Norm()
94- rr := Ray{Origin: hit.Point.Add(hit.Normal.Scale(1e-4)), Dir: r}
95- col = col.Add(p.trace(rr, depth+1, rng).Scale(mat.Reflectivity))
96- }
97-
98- // Glass: bend the ray through the surface and keep going.
99- if mat.Transparency > 0 {
100- ior := 1.0
101- if mat.IOR > 0 {
102- ior = mat.IOR
103- }
104- if refr, ok := refract(ray.Dir, hit.Normal, 1/ior); ok {
105- tr := Ray{Origin: hit.Point.Add(hit.Normal.Scale(1e-4)), Dir: refr}
106- col = col.Add(p.trace(tr, depth+1, rng).Scale(mat.Transparency))
107- }
108- }
109-
110- // Diffuse: scatter in a cosine-weighted random direction. This is
111- // what carries light from one surface to another (global
112- // illumination).
113- if depth < 8 && (mat.Diffuse.R > 0 || mat.Diffuse.G > 0 || mat.Diffuse.B > 0) {
114- d := cosineSample(hit.Normal, rng)
115- dr := Ray{Origin: hit.Point.Add(hit.Normal.Scale(1e-4)), Dir: d}
116- col = col.Add(p.trace(dr, depth+1, rng).Mul(mat.Diffuse))
117- }
118-
119- // Russian roulette: terminate the path with probability 1-max(col),
120- // unbiased.
121- q := math.Max(col.R, math.Max(col.G, col.B))
122- q = math.Min(1, math.Max(q, 0.05))
123- if rng.Float64() > q {
124- return col
125- }
126- return col.Scale(1 / q)
127-}
128-
129-// cosineSample returns a random direction in the hemisphere around n,
130-// weighted by cos(theta) — the distribution a perfect diffuse surface
131-// scatters into.
132-func cosineSample(n Vec3, rng *rand.Rand) Vec3 {
133- // A random point on the unit disk, mapped onto the hemisphere.
134- r := math.Sqrt(rng.Float64())
135- phi := 2 * math.Pi * rng.Float64()
136- rx, ry := r*math.Cos(phi), r*math.Sin(phi)
137- rz := math.Sqrt(math.Max(0, 1-r*r))
138- // A basis with n as its z axis.
139- var up Vec3
140- if math.Abs(n.Y) < 0.99 {
141- up = Vec3{0, 1, 0}
142- } else {
143- up = Vec3{1, 0, 0}
144- }
145- u := n.Cross(up).Norm()
146- v := n.Cross(u)
147- return u.Scale(rx).Add(v.Scale(ry)).Add(n.Scale(rz))
148-}
deleted file mode 100644
@@ -1,148 +0,0 @@
1-package povray
2-
3-import (
4- "math"
5- "math/rand/v2"
6- "sync"
7-)
8-
9-// PathTracer renders a scene with path tracing: each ray bounces around
10-// the scene, scattering diffusely at matte surfaces, until it hits an
11-// emissive surface or leaves the scene. This gives global illumination
12-// and soft shadows, at the cost of a longer render (use Samples to
13-// control quality vs. time).
14-//
15-// Lights for path tracing are emissive surfaces: give an object a
16-// material with Emission set (see LightMaterial). Point lights in
17-// Scene.Lights are ignored by the path tracer.
18-type PathTracer struct {
19- Scene *Scene
20- // Width and Height of the image in pixels.
21- Width, Height int
22- // Samples is the number of primary rays per pixel.
23- Samples int
24- // Threads is the number of worker goroutines (0 = 4).
25- Threads int
26-}
27-
28-// Render renders the scene and returns the image. Each worker goroutine
29-// uses its own random stream, so a given configuration always produces
30-// the same image.
31-func (p *PathTracer) Render(cam Camera) *Image {
32- samples := p.Samples
33- if samples < 1 {
34- samples = 1
35- }
36- threads := p.Threads
37- if threads <= 0 {
38- threads = 4
39- }
40- img := NewImage(p.Width, p.Height)
41-
42- var wg sync.WaitGroup
43- rowsPer := (p.Height + threads - 1) / threads
44- for c := 0; c < threads; c++ {
45- y0, y1 := c*rowsPer, (c+1)*rowsPer
46- if y1 > p.Height {
47- y1 = p.Height
48- }
49- if y0 >= y1 {
50- break
51- }
52- wg.Add(1)
53- go func(y0, y1 int, rng *rand.Rand) {
54- defer wg.Done()
55- for y := y0; y < y1; y++ {
56- for x := 0; x < p.Width; x++ {
57- var col Color
58- for smp := 0; smp < samples; smp++ {
59- u := float64(x) + rng.Float64()
60- v := float64(y) + rng.Float64()
61- col = col.Add(p.trace(cam.RayAtUV(u, v, p.Width, p.Height), 0, rng))
62- }
63- img.Set(x, y, col.Scale(1.0/float64(samples)))
64- }
65- }
66- }(y0, y1, rand.New(rand.NewPCG(uint64(12345+c), uint64(987654321+c))))
67- }
68- wg.Wait()
69- return img
70-}
71-
72-// trace follows one path through the scene and returns the accumulated
73-// radiance.
74-func (p *PathTracer) trace(ray Ray, depth int, rng *rand.Rand) Color {
75- // Hard bounce limit: glass and mirrors can bounce a ray forever;
76- // past 50 bounces the remaining contribution is negligible.
77- if depth > 50 {
78- return Black
79- }
80- s := p.Scene
81- idx, hit := closestHit(s.Objects, ray)
82- if idx < 0 {
83- return s.Background
84- }
85- hit.Flip(ray)
86- mat := s.Materials[idx]
87-
88- // An emissive surface radiates its own light.
89- col := mat.Emission
90-
91- // Specular: mirror the ray and keep going.
92- if mat.Reflectivity > 0 {
93- r := ray.Dir.Sub(hit.Normal.Scale(2 * ray.Dir.Dot(hit.Normal))).Norm()
94- rr := Ray{Origin: hit.Point.Add(hit.Normal.Scale(1e-4)), Dir: r}
95- col = col.Add(p.trace(rr, depth+1, rng).Scale(mat.Reflectivity))
96- }
97-
98- // Glass: bend the ray through the surface and keep going.
99- if mat.Transparency > 0 {
100- ior := 1.0
101- if mat.IOR > 0 {
102- ior = mat.IOR
103- }
104- if refr, ok := refract(ray.Dir, hit.Normal, 1/ior); ok {
105- tr := Ray{Origin: hit.Point.Add(hit.Normal.Scale(1e-4)), Dir: refr}
106- col = col.Add(p.trace(tr, depth+1, rng).Scale(mat.Transparency))
107- }
108- }
109-
110- // Diffuse: scatter in a cosine-weighted random direction. This is
111- // what carries light from one surface to another (global
112- // illumination).
113- if depth < 8 && (mat.Diffuse.R > 0 || mat.Diffuse.G > 0 || mat.Diffuse.B > 0) {
114- d := cosineSample(hit.Normal, rng)
115- dr := Ray{Origin: hit.Point.Add(hit.Normal.Scale(1e-4)), Dir: d}
116- col = col.Add(p.trace(dr, depth+1, rng).Mul(mat.Diffuse))
117- }
118-
119- // Russian roulette: terminate the path with probability 1-max(col),
120- // unbiased.
121- q := math.Max(col.R, math.Max(col.G, col.B))
122- q = math.Min(1, math.Max(q, 0.05))
123- if rng.Float64() > q {
124- return col
125- }
126- return col.Scale(1 / q)
127-}
128-
129-// cosineSample returns a random direction in the hemisphere around n,
130-// weighted by cos(theta) — the distribution a perfect diffuse surface
131-// scatters into.
132-func cosineSample(n Vec3, rng *rand.Rand) Vec3 {
133- // A random point on the unit disk, mapped onto the hemisphere.
134- r := math.Sqrt(rng.Float64())
135- phi := 2 * math.Pi * rng.Float64()
136- rx, ry := r*math.Cos(phi), r*math.Sin(phi)
137- rz := math.Sqrt(math.Max(0, 1-r*r))
138- // A basis with n as its z axis.
139- var up Vec3
140- if math.Abs(n.Y) < 0.99 {
141- up = Vec3{0, 1, 0}
142- } else {
143- up = Vec3{1, 0, 0}
144- }
145- u := n.Cross(up).Norm()
146- v := n.Cross(u)
147- return u.Scale(rx).Add(v.Scale(ry)).Add(n.Scale(rz))
148-}
deleted demos/qwen-style/workspace/png.go +0 -62
deleted file mode 100644
@@ -1,62 +0,0 @@
1-package povray
2-
3-import (
4- "bytes"
5- "compress/zlib"
6- "encoding/binary"
7- "hash/crc32"
8- "os"
9-)
10-
11-// SavePNG writes the image as a PNG file (truecolor, 8 bits per channel).
12-// It uses only the standard library.
13-func (img *Image) SavePNG(path string) error {
14- var body bytes.Buffer
15-
16- // Signature.
17- body.Write([]byte{137, 80, 78, 71, 13, 10, 26, 10})
18-
19- // IHDR: width, height, bit depth 8, color type 2 (RGB),
20- // compression 0, filter 0, interlace 0.
21- var ihdr [13]byte
22- binary.BigEndian.PutUint32(ihdr[0:4], uint32(img.Width))
23- binary.BigEndian.PutUint32(ihdr[4:8], uint32(img.Height))
24- ihdr[8] = 8 // bit depth
25- ihdr[9] = 2 // color type: truecolor
26- body.Write(pngChunk("IHDR", ihdr[:]))
27-
28- // IDAT: each scanline prefixed with filter byte 0, then zlib-compressed.
29- raw := make([]byte, 0, img.Height*(1+img.Width*3))
30- for y := 0; y < img.Height; y++ {
31- raw = append(raw, 0)
32- row := img.Pix[y*img.Width*3 : (y+1)*img.Width*3]
33- raw = append(raw, row...)
34- }
35- var compressed bytes.Buffer
36- zw, err := zlib.NewWriterLevel(&compressed, zlib.DefaultCompression)
37- if err != nil {
38- return err
39- }
40- if _, err := zw.Write(raw); err != nil {
41- return err
42- }
43- if err := zw.Close(); err != nil {
44- return err
45- }
46- body.Write(pngChunk("IDAT", compressed.Bytes()))
47-
48- // IEND.
49- body.Write(pngChunk("IEND", nil))
50-
51- return os.WriteFile(path, body.Bytes(), 0o644)
52-}
53-
54-// pngChunk builds a PNG chunk: 4-byte length, type, data, CRC32(type+data).
55-func pngChunk(typ string, data []byte) []byte {
56- out := make([]byte, 12+len(data))
57- binary.BigEndian.PutUint32(out[0:4], uint32(len(data)))
58- copy(out[4:8], typ)
59- copy(out[8:], data)
60- binary.BigEndian.PutUint32(out[8+len(data):], crc32.ChecksumIEEE(out[4:8+len(data)]))
61- return out
62-}
deleted file mode 100644
@@ -1,62 +0,0 @@
1-package povray
2-
3-import (
4- "bytes"
5- "compress/zlib"
6- "encoding/binary"
7- "hash/crc32"
8- "os"
9-)
10-
11-// SavePNG writes the image as a PNG file (truecolor, 8 bits per channel).
12-// It uses only the standard library.
13-func (img *Image) SavePNG(path string) error {
14- var body bytes.Buffer
15-
16- // Signature.
17- body.Write([]byte{137, 80, 78, 71, 13, 10, 26, 10})
18-
19- // IHDR: width, height, bit depth 8, color type 2 (RGB),
20- // compression 0, filter 0, interlace 0.
21- var ihdr [13]byte
22- binary.BigEndian.PutUint32(ihdr[0:4], uint32(img.Width))
23- binary.BigEndian.PutUint32(ihdr[4:8], uint32(img.Height))
24- ihdr[8] = 8 // bit depth
25- ihdr[9] = 2 // color type: truecolor
26- body.Write(pngChunk("IHDR", ihdr[:]))
27-
28- // IDAT: each scanline prefixed with filter byte 0, then zlib-compressed.
29- raw := make([]byte, 0, img.Height*(1+img.Width*3))
30- for y := 0; y < img.Height; y++ {
31- raw = append(raw, 0)
32- row := img.Pix[y*img.Width*3 : (y+1)*img.Width*3]
33- raw = append(raw, row...)
34- }
35- var compressed bytes.Buffer
36- zw, err := zlib.NewWriterLevel(&compressed, zlib.DefaultCompression)
37- if err != nil {
38- return err
39- }
40- if _, err := zw.Write(raw); err != nil {
41- return err
42- }
43- if err := zw.Close(); err != nil {
44- return err
45- }
46- body.Write(pngChunk("IDAT", compressed.Bytes()))
47-
48- // IEND.
49- body.Write(pngChunk("IEND", nil))
50-
51- return os.WriteFile(path, body.Bytes(), 0o644)
52-}
53-
54-// pngChunk builds a PNG chunk: 4-byte length, type, data, CRC32(type+data).
55-func pngChunk(typ string, data []byte) []byte {
56- out := make([]byte, 12+len(data))
57- binary.BigEndian.PutUint32(out[0:4], uint32(len(data)))
58- copy(out[4:8], typ)
59- copy(out[8:], data)
60- binary.BigEndian.PutUint32(out[8+len(data):], crc32.ChecksumIEEE(out[4:8+len(data)]))
61- return out
62-}
deleted demos/qwen-style/workspace/povray_test.go +0 -336
deleted file mode 100644
@@ -1,336 +0,0 @@
1-package povray
2-
3-import (
4- "bytes"
5- "encoding/binary"
6- "math"
7- "os"
8- "testing"
9-)
10-
11-func TestSphereIntersect(t *testing.T) {
12- s := Sphere{Center: Vec3{0, 0, 0}, Radius: 1}
13- // Head-on hit.
14- h, ok := s.Intersect(Ray{Origin: Vec3{0, 0, 5}, Dir: Vec3{0, 0, -1}})
15- if !ok {
16- t.Fatal("expected a hit")
17- }
18- if h.Dist != 4 {
19- t.Errorf("dist = %v, want 4", h.Dist)
20- }
21- if h.Point.Z != 1 {
22- t.Errorf("point = %v, want z=1", h.Point)
23- }
24- // Miss.
25- if _, ok := s.Intersect(Ray{Origin: Vec3{0, 5, 5}, Dir: Vec3{0, 0, -1}}); ok {
26- t.Error("expected a miss")
27- }
28- // Ray starting inside: should still hit the far side.
29- h, ok = s.Intersect(Ray{Origin: Vec3{0, 0, 0}, Dir: Vec3{0, 0, -1}})
30- if !ok || h.Dist <= 0 {
31- t.Errorf("inside hit = %+v, %v", h, ok)
32- }
33-}
34-
35-func TestPlaneIntersect(t *testing.T) {
36- p := NewPlaneXY(2)
37- h, ok := p.Intersect(Ray{Origin: Vec3{0, 0, 0}, Dir: Vec3{0, 1, 0}})
38- if !ok || h.Dist != 2 {
39- t.Errorf("hit = %+v, %v", h, ok)
40- }
41- // Parallel ray misses.
42- if _, ok := p.Intersect(Ray{Origin: Vec3{0, 0, 0}, Dir: Vec3{1, 0, 0}}); ok {
43- t.Error("expected a miss for parallel ray")
44- }
45-}
46-
47-func TestBoxIntersect(t *testing.T) {
48- b := Box{Min: Vec3{-1, -1, -1}, Max: Vec3{1, 1, 1}}
49- h, ok := b.Intersect(Ray{Origin: Vec3{0, 0, 5}, Dir: Vec3{0, 0, -1}})
50- if !ok || h.Dist != 4 || h.Normal.Z != 1 {
51- t.Errorf("hit = %+v, %v", h, ok)
52- }
53- if _, ok := b.Intersect(Ray{Origin: Vec3{5, 0, 0}, Dir: Vec3{0, 0, -1}}); ok {
54- t.Error("expected a miss")
55- }
56-}
57-
58-func TestGroupClosest(t *testing.T) {
59- g := Group{Objects: []Object{
60- Sphere{Center: Vec3{0, 0, -2}, Radius: 0.5},
61- Sphere{Center: Vec3{0, 0, -5}, Radius: 0.5},
62- }}
63- h, ok := g.Intersect(Ray{Origin: Vec3{0, 0, 0}, Dir: Vec3{0, 0, -1}})
64- if !ok || h.Dist != 1.5 {
65- t.Errorf("expected closest hit at 1.5, got %+v, %v", h, ok)
66- }
67-}
68-
69-func TestRefract(t *testing.T) {
70- // Straight through: direction parallel to normal is unchanged.
71- d, ok := refract(Vec3{0, 0, -1}, Vec3{0, 0, 1}, 1.5)
72- if !ok || d.Z >= 0 {
73- t.Errorf("got %+v, %v", d, ok)
74- }
75- // Grazing incidence: total internal reflection.
76- if _, ok := refract(Vec3{0.999, 0, -0.04}, Vec3{0, 0, 1}, 1.5); ok {
77- t.Error("expected total internal reflection")
78- }
79-}
80-
81-func TestTransformMat4(t *testing.T) {
82- // Identity leaves points and dirs alone.
83- if p := Identity().TransformPoint(Vec3{1, 2, 3}); p != (Vec3{1, 2, 3}) {
84- t.Errorf("identity point = %v", p)
85- }
86- // Translate moves a point, not a direction.
87- m := Translate(1, -2, 3)
88- if p := m.TransformPoint(Vec3{0, 0, 0}); p != (Vec3{1, -2, 3}) {
89- t.Errorf("translate point = %v", p)
90- }
91- if d := m.TransformDir(Vec3{1, 0, 0}); d != (Vec3{1, 0, 0}) {
92- t.Errorf("translate dir = %v", d)
93- }
94- // Scale stretches a direction.
95- if d := Scale(2, 3, 4).TransformDir(Vec3{1, 1, 1}); d != (Vec3{2, 3, 4}) {
96- t.Errorf("scale dir = %v", d)
97- }
98- // RotateY by pi/2 sends +Z to +X.
99- d := RotateY(math.Pi / 2).TransformDir(Vec3{0, 0, 1})
100- if math.Abs(d.X-1) > 1e-9 || math.Abs(d.Z) > 1e-9 {
101- t.Errorf("rotateY dir = %v", d)
102- }
103- // Multiply: translate then scale (scale first, then translate).
104- m = Translate(1, 0, 0).Multiply(Scale(2, 2, 2))
105- if p := m.TransformPoint(Vec3{1, 0, 0}); p != (Vec3{3, 0, 0}) {
106- t.Errorf("composed point = %v", p)
107- }
108- // Inverse round-trips a point; singular matrix reports failure.
109- m = Translate(1, 2, 3).Multiply(RotateX(0.7)).Multiply(Scale(2, 3, 4))
110- inv, ok := m.Inverse()
111- if !ok {
112- t.Fatal("expected an inverse")
113- }
114- p := Vec3{1.3, -0.4, 2.2}
115- if rt := inv.TransformPoint(m.TransformPoint(p)); rt.Sub(p).Len() > 1e-9 {
116- t.Errorf("round trip = %v", rt)
117- }
118- if _, ok := Scale(0, 1, 1).Inverse(); ok {
119- t.Error("expected singular matrix")
120- }
121- // Non-uniform scale: normals go through the inverse transpose.
122- // A sphere scaled by (2,1,1) is the ellipsoid x²/4+y²+z²=1; at the
123- // point where the local normal is (1,1,0), the world normal is
124- // (0.5,1,0)/|(0.5,1,0)| = (0.447, 0.894, 0).
125- n := Scale(2, 1, 1).TransformNormal(Vec3{1, 1, 0})
126- want := Vec3{0.5, 1, 0}.Norm()
127- if n.Sub(want).Len() > 1e-9 {
128- t.Errorf("scaled normal = %v, want %v", n, want)
129- }
130-}
131-
132-func TestTransformObject(t *testing.T) {
133- // A translated sphere: hit at the shifted point, normal preserved.
134- s := Transform{Object: Sphere{Center: Vec3{0, 0, 0}, Radius: 1}, Matrix: Translate(0, 0, -5)}
135- h, ok := s.Intersect(Ray{Origin: Vec3{0, 0, 0}, Dir: Vec3{0, 0, -1}})
136- if !ok || math.Abs(h.Dist-4) > 1e-6 || h.Point.Z != -4 {
137- t.Errorf("hit = %+v, %v", h, ok)
138- }
139- if h.Normal.Z != 1 {
140- t.Errorf("normal = %v, want (0,0,1)", h.Normal)
141- }
142- // A box rotated 45° around Z is symmetric about the Z axis, so a
143- // head-on -Z ray still hits the front face at the same distance.
144- b := Transform{Object: Box{Min: Vec3{-1, -1, -1}, Max: Vec3{1, 1, 1}}, Matrix: RotateZ(math.Pi / 4)}
145- h, ok = b.Intersect(Ray{Origin: Vec3{0, 0, 5}, Dir: Vec3{0, 0, -1}})
146- if !ok || math.Abs(h.Dist-4) > 1e-6 {
147- t.Errorf("rotated box hit = %+v, %v", h, ok)
148- }
149- // Rotate the box 45° around X. A -Z ray at y=0.5 strikes the tilted
150- // former top face (local y=1); its world normal is RotateX(45°)·(0,1,0)
151- // = (0, cos45, sin45).
152- b = Transform{Object: Box{Min: Vec3{-1, -1, -1}, Max: Vec3{1, 1, 1}}, Matrix: RotateX(math.Pi / 4)}
153- h, ok = b.Intersect(Ray{Origin: Vec3{0, 0.5, 5}, Dir: Vec3{0, 0, -1}})
154- if !ok {
155- t.Fatal("expected a hit on the rotated box")
156- }
157- if math.Abs(h.Normal.Y-math.Sqrt2/2) > 1e-6 || math.Abs(h.Normal.Z-math.Sqrt2/2) > 1e-6 {
158- t.Errorf("rotated normal = %v, want (0, 0.707, 0.707)", h.Normal)
159- }
160- // Miss: a ray that misses the local object misses the transformed one.
161- if _, ok := s.Intersect(Ray{Origin: Vec3{3, 0, 0}, Dir: Vec3{0, 0, -1}}); ok {
162- t.Error("expected a miss")
163- }
164-}
165-
166-func TestRayAtUV(t *testing.T) {
167- cam := NewCamera(Vec3{0, 0, 5})
168- // The center of an odd-sized image aims straight at the look-at point.
169- c := cam.RayAt(5, 5, 11, 11)
170- if math.Abs(c.Dir.X) > 1e-9 || math.Abs(c.Dir.Y) > 1e-9 || c.Dir.Z > 0 {
171- t.Errorf("center ray = %v, want (0,0,-1)", c.Dir)
172- }
173- // RayAt(px, py) is the center of the pixel: same as RayAtUV(px+0.5, py+0.5).
174- c2 := cam.RayAtUV(5.5, 5.5, 11, 11)
175- if c.Dir.Sub(c2.Dir).Len() > 1e-12 {
176- t.Errorf("RayAt != RayAtUV center: %v vs %v", c.Dir, c2.Dir)
177- }
178- // A sample in the top-left quadrant points up and left.
179- tl := cam.RayAtUV(0.25, 0.25, 11, 11)
180- if tl.Dir.X >= 0 || tl.Dir.Y <= 0 {
181- t.Errorf("top-left sample = %v, want X<0, Y>0", tl.Dir)
182- }
183- // A sample in the bottom-right quadrant points down and right.
184- br := cam.RayAtUV(10.75, 10.75, 11, 11)
185- if br.Dir.X <= 0 || br.Dir.Y >= 0 {
186- t.Errorf("bottom-right sample = %v, want X>0, Y<0", br.Dir)
187- }
188-}
189-
190-func TestSupersampling(t *testing.T) {
191- // A scene with a hard edge: a red sphere against a mid-gray
192- // background. With 1 sample every pixel is pure red or pure gray;
193- // with 2x2 samples the silhouette pixels must be a blend.
194- build := func() *Scene {
195- s := NewScene()
196- s.AddObject(Sphere{Center: Vec3{0, 0, 0}, Radius: 1},
197- Material{Diffuse: Color{1, 0, 0}, Ambient: 1})
198- s.Background = Color{0.5, 0.5, 0.5}
199- return s
200- }
201- cam := NewCamera(Vec3{0, 0, 5})
202-
203- // Count pixels in row 20 (where the sphere's silhouette has a steep
204- // slope, so a pixel's 2x2 sub-samples straddle the edge) whose green
205- // channel is strictly between the sphere (pure red, G=0) and the
206- // background (gray, G≈186 after gamma): a "mixed" pixel is an
207- // anti-aliased edge.
208- mixed := func(img *Image) int {
209- var n int
210- for x := 0; x < img.Width; x++ {
211- i := (20*img.Width + x) * 3
212- if img.Pix[i+1] > 5 && img.Pix[i+1] < 181 {
213- n++
214- }
215- }
216- return n
217- }
218-
219- r1 := Renderer{Scene: build(), Width: 64, Height: 48, Samples: 1}
220- if m := mixed(r1.Render(cam)); m > 0 {
221- t.Errorf("1-sample render has %d mixed pixels, want 0", m)
222- }
223-
224- r2 := Renderer{Scene: build(), Width: 64, Height: 48, Samples: 2}
225- if m := mixed(r2.Render(cam)); m == 0 {
226- t.Error("2x2 render has no mixed pixels: edge not anti-aliased")
227- }
228-}
229-
230-func TestPathTracer(t *testing.T) {
231- // A room: white floor, a sphere, and an emissive panel above.
232- // Path tracing must light the floor from the panel, and the floor
233- // behind the sphere (toward the camera) must be in shadow.
234- build := func() *Scene {
235- s := NewScene()
236- s.Background = Black
237- s.AddObject(NewPlaneXY(0), Material{Diffuse: White})
238- s.AddObject(Sphere{Center: Vec3{0, 1, 0}, Radius: 1}, Material{Diffuse: White})
239- // A downward-facing emissive panel.
240- panel := Transform{
241- Object: Box{Min: Vec3{-2, 0, -2}, Max: Vec3{2, 0.1, 2}},
242- Matrix: Translate(0, 8, 0),
243- }
244- s.AddObject(panel, LightMaterial(White, 12))
245- return s
246- }
247- cam := NewCamera(Vec3{0, 2, 8})
248- p := PathTracer{Scene: build(), Width: 64, Height: 48, Samples: 8}
249- img := p.Render(cam)
250-
251- // The floor occupies the bottom half of the image. It must be
252- // lit (not black) and must have a bright region (direct light from
253- // the panel) and a dark region (the sphere's shadow).
254- var min, max byte
255- for y := 24; y < 48; y++ {
256- for x := 0; x < 64; x++ {
257- v := img.Pix[(y*64+x)*3]
258- if x == 0 || v < min {
259- min = v
260- }
261- if x == 0 || v > max {
262- max = v
263- }
264- }
265- }
266- if max < 40 {
267- t.Errorf("floor not lit by the panel: max = %d", max)
268- }
269- if float64(max) < 2*float64(min+1) {
270- t.Errorf("no shadow visible: min = %d, max = %d", min, max)
271- }
272-
273- // Determinism: the same configuration must give the same image.
274- img2 := p.Render(cam)
275- if !bytes.Equal(img.Pix, img2.Pix) {
276- t.Error("two renders of the same scene differ")
277- }
278-}
279-
280-func TestRenderSmoke(t *testing.T) {
281- s := NewScene()
282- s.AddObject(Sphere{Center: Vec3{0, 1, 0}, Radius: 1},
283- Material{Diffuse: Red, Specular: White, Shininess: 30, Ambient: 0.1})
284- s.AddObject(NewPlaneXY(0), Material{Diffuse: Gray, Ambient: 0.3})
285- s.Lights = []Light{NewLight(Vec3{4, 8, 6}, White)}
286-
287- cam := NewCamera(Vec3{0, 2, 8})
288- r := Renderer{Scene: s, Width: 64, Height: 48}
289- img := r.Render(cam)
290- if img.Width != 64 || img.Height != 48 {
291- t.Fatalf("image size = %dx%d", img.Width, img.Height)
292- }
293- // The center pixel should hit the red sphere: red channel dominant.
294- i := (24*64 + 32) * 3
295- if img.Pix[i] < 60 {
296- t.Errorf("center pixel too dark: %v", img.Pix[i:i+3])
297- }
298- if img.Pix[i+1] > img.Pix[i] || img.Pix[i+2] > img.Pix[i] {
299- t.Errorf("center pixel not red-dominant: %v", img.Pix[i:i+3])
300- }
301-}
302-
303-func TestSavePNG(t *testing.T) {
304- img := NewImage(4, 3)
305- for y := 0; y < 3; y++ {
306- for x := 0; x < 4; x++ {
307- img.Set(x, y, Color{float64(x) / 3, float64(y) / 2, 1})
308- }
309- }
310- path := t.TempDir() + "/test.png"
311- if err := img.SavePNG(path); err != nil {
312- t.Fatal(err)
313- }
314- data, err := os.ReadFile(path)
315- if err != nil {
316- t.Fatal(err)
317- }
318- // PNG signature.
319- sig := []byte{137, 80, 78, 71, 13, 10, 26, 10}
320- if !bytes.Equal(data[:8], sig) {
321- t.Fatalf("bad signature: %v", data[:8])
322- }
323- // IHDR must say 4x3, 8-bit truecolor.
324- if binary.BigEndian.Uint32(data[16:20]) != 4 || binary.BigEndian.Uint32(data[20:24]) != 3 {
325- t.Errorf("IHDR size wrong: %v", data[16:24])
326- }
327- if data[24] != 8 || data[25] != 2 {
328- t.Errorf("IHDR depth/type wrong: %d %d", data[24], data[25])
329- }
330- // The file must end with an IEND chunk: length 0, "IEND", CRC AE426082.
331- if !bytes.Equal(data[len(data)-8:len(data)-4], []byte("IEND")) ||
332- !bytes.Equal(data[len(data)-4:], []byte{0xAE, 0x42, 0x60, 0x82}) ||
333- !bytes.Equal(data[len(data)-12:len(data)-8], []byte{0, 0, 0, 0}) {
334- t.Errorf("missing IEND trailer: %v", data[len(data)-12:])
335- }
336-}
deleted file mode 100644
@@ -1,336 +0,0 @@
1-package povray
2-
3-import (
4- "bytes"
5- "encoding/binary"
6- "math"
7- "os"
8- "testing"
9-)
10-
11-func TestSphereIntersect(t *testing.T) {
12- s := Sphere{Center: Vec3{0, 0, 0}, Radius: 1}
13- // Head-on hit.
14- h, ok := s.Intersect(Ray{Origin: Vec3{0, 0, 5}, Dir: Vec3{0, 0, -1}})
15- if !ok {
16- t.Fatal("expected a hit")
17- }
18- if h.Dist != 4 {
19- t.Errorf("dist = %v, want 4", h.Dist)
20- }
21- if h.Point.Z != 1 {
22- t.Errorf("point = %v, want z=1", h.Point)
23- }
24- // Miss.
25- if _, ok := s.Intersect(Ray{Origin: Vec3{0, 5, 5}, Dir: Vec3{0, 0, -1}}); ok {
26- t.Error("expected a miss")
27- }
28- // Ray starting inside: should still hit the far side.
29- h, ok = s.Intersect(Ray{Origin: Vec3{0, 0, 0}, Dir: Vec3{0, 0, -1}})
30- if !ok || h.Dist <= 0 {
31- t.Errorf("inside hit = %+v, %v", h, ok)
32- }
33-}
34-
35-func TestPlaneIntersect(t *testing.T) {
36- p := NewPlaneXY(2)
37- h, ok := p.Intersect(Ray{Origin: Vec3{0, 0, 0}, Dir: Vec3{0, 1, 0}})
38- if !ok || h.Dist != 2 {
39- t.Errorf("hit = %+v, %v", h, ok)
40- }
41- // Parallel ray misses.
42- if _, ok := p.Intersect(Ray{Origin: Vec3{0, 0, 0}, Dir: Vec3{1, 0, 0}}); ok {
43- t.Error("expected a miss for parallel ray")
44- }
45-}
46-
47-func TestBoxIntersect(t *testing.T) {
48- b := Box{Min: Vec3{-1, -1, -1}, Max: Vec3{1, 1, 1}}
49- h, ok := b.Intersect(Ray{Origin: Vec3{0, 0, 5}, Dir: Vec3{0, 0, -1}})
50- if !ok || h.Dist != 4 || h.Normal.Z != 1 {
51- t.Errorf("hit = %+v, %v", h, ok)
52- }
53- if _, ok := b.Intersect(Ray{Origin: Vec3{5, 0, 0}, Dir: Vec3{0, 0, -1}}); ok {
54- t.Error("expected a miss")
55- }
56-}
57-
58-func TestGroupClosest(t *testing.T) {
59- g := Group{Objects: []Object{
60- Sphere{Center: Vec3{0, 0, -2}, Radius: 0.5},
61- Sphere{Center: Vec3{0, 0, -5}, Radius: 0.5},
62- }}
63- h, ok := g.Intersect(Ray{Origin: Vec3{0, 0, 0}, Dir: Vec3{0, 0, -1}})
64- if !ok || h.Dist != 1.5 {
65- t.Errorf("expected closest hit at 1.5, got %+v, %v", h, ok)
66- }
67-}
68-
69-func TestRefract(t *testing.T) {
70- // Straight through: direction parallel to normal is unchanged.
71- d, ok := refract(Vec3{0, 0, -1}, Vec3{0, 0, 1}, 1.5)
72- if !ok || d.Z >= 0 {
73- t.Errorf("got %+v, %v", d, ok)
74- }
75- // Grazing incidence: total internal reflection.
76- if _, ok := refract(Vec3{0.999, 0, -0.04}, Vec3{0, 0, 1}, 1.5); ok {
77- t.Error("expected total internal reflection")
78- }
79-}
80-
81-func TestTransformMat4(t *testing.T) {
82- // Identity leaves points and dirs alone.
83- if p := Identity().TransformPoint(Vec3{1, 2, 3}); p != (Vec3{1, 2, 3}) {
84- t.Errorf("identity point = %v", p)
85- }
86- // Translate moves a point, not a direction.
87- m := Translate(1, -2, 3)
88- if p := m.TransformPoint(Vec3{0, 0, 0}); p != (Vec3{1, -2, 3}) {
89- t.Errorf("translate point = %v", p)
90- }
91- if d := m.TransformDir(Vec3{1, 0, 0}); d != (Vec3{1, 0, 0}) {
92- t.Errorf("translate dir = %v", d)
93- }
94- // Scale stretches a direction.
95- if d := Scale(2, 3, 4).TransformDir(Vec3{1, 1, 1}); d != (Vec3{2, 3, 4}) {
96- t.Errorf("scale dir = %v", d)
97- }
98- // RotateY by pi/2 sends +Z to +X.
99- d := RotateY(math.Pi / 2).TransformDir(Vec3{0, 0, 1})
100- if math.Abs(d.X-1) > 1e-9 || math.Abs(d.Z) > 1e-9 {
101- t.Errorf("rotateY dir = %v", d)
102- }
103- // Multiply: translate then scale (scale first, then translate).
104- m = Translate(1, 0, 0).Multiply(Scale(2, 2, 2))
105- if p := m.TransformPoint(Vec3{1, 0, 0}); p != (Vec3{3, 0, 0}) {
106- t.Errorf("composed point = %v", p)
107- }
108- // Inverse round-trips a point; singular matrix reports failure.
109- m = Translate(1, 2, 3).Multiply(RotateX(0.7)).Multiply(Scale(2, 3, 4))
110- inv, ok := m.Inverse()
111- if !ok {
112- t.Fatal("expected an inverse")
113- }
114- p := Vec3{1.3, -0.4, 2.2}
115- if rt := inv.TransformPoint(m.TransformPoint(p)); rt.Sub(p).Len() > 1e-9 {
116- t.Errorf("round trip = %v", rt)
117- }
118- if _, ok := Scale(0, 1, 1).Inverse(); ok {
119- t.Error("expected singular matrix")
120- }
121- // Non-uniform scale: normals go through the inverse transpose.
122- // A sphere scaled by (2,1,1) is the ellipsoid x²/4+y²+z²=1; at the
123- // point where the local normal is (1,1,0), the world normal is
124- // (0.5,1,0)/|(0.5,1,0)| = (0.447, 0.894, 0).
125- n := Scale(2, 1, 1).TransformNormal(Vec3{1, 1, 0})
126- want := Vec3{0.5, 1, 0}.Norm()
127- if n.Sub(want).Len() > 1e-9 {
128- t.Errorf("scaled normal = %v, want %v", n, want)
129- }
130-}
131-
132-func TestTransformObject(t *testing.T) {
133- // A translated sphere: hit at the shifted point, normal preserved.
134- s := Transform{Object: Sphere{Center: Vec3{0, 0, 0}, Radius: 1}, Matrix: Translate(0, 0, -5)}
135- h, ok := s.Intersect(Ray{Origin: Vec3{0, 0, 0}, Dir: Vec3{0, 0, -1}})
136- if !ok || math.Abs(h.Dist-4) > 1e-6 || h.Point.Z != -4 {
137- t.Errorf("hit = %+v, %v", h, ok)
138- }
139- if h.Normal.Z != 1 {
140- t.Errorf("normal = %v, want (0,0,1)", h.Normal)
141- }
142- // A box rotated 45° around Z is symmetric about the Z axis, so a
143- // head-on -Z ray still hits the front face at the same distance.
144- b := Transform{Object: Box{Min: Vec3{-1, -1, -1}, Max: Vec3{1, 1, 1}}, Matrix: RotateZ(math.Pi / 4)}
145- h, ok = b.Intersect(Ray{Origin: Vec3{0, 0, 5}, Dir: Vec3{0, 0, -1}})
146- if !ok || math.Abs(h.Dist-4) > 1e-6 {
147- t.Errorf("rotated box hit = %+v, %v", h, ok)
148- }
149- // Rotate the box 45° around X. A -Z ray at y=0.5 strikes the tilted
150- // former top face (local y=1); its world normal is RotateX(45°)·(0,1,0)
151- // = (0, cos45, sin45).
152- b = Transform{Object: Box{Min: Vec3{-1, -1, -1}, Max: Vec3{1, 1, 1}}, Matrix: RotateX(math.Pi / 4)}
153- h, ok = b.Intersect(Ray{Origin: Vec3{0, 0.5, 5}, Dir: Vec3{0, 0, -1}})
154- if !ok {
155- t.Fatal("expected a hit on the rotated box")
156- }
157- if math.Abs(h.Normal.Y-math.Sqrt2/2) > 1e-6 || math.Abs(h.Normal.Z-math.Sqrt2/2) > 1e-6 {
158- t.Errorf("rotated normal = %v, want (0, 0.707, 0.707)", h.Normal)
159- }
160- // Miss: a ray that misses the local object misses the transformed one.
161- if _, ok := s.Intersect(Ray{Origin: Vec3{3, 0, 0}, Dir: Vec3{0, 0, -1}}); ok {
162- t.Error("expected a miss")
163- }
164-}
165-
166-func TestRayAtUV(t *testing.T) {
167- cam := NewCamera(Vec3{0, 0, 5})
168- // The center of an odd-sized image aims straight at the look-at point.
169- c := cam.RayAt(5, 5, 11, 11)
170- if math.Abs(c.Dir.X) > 1e-9 || math.Abs(c.Dir.Y) > 1e-9 || c.Dir.Z > 0 {
171- t.Errorf("center ray = %v, want (0,0,-1)", c.Dir)
172- }
173- // RayAt(px, py) is the center of the pixel: same as RayAtUV(px+0.5, py+0.5).
174- c2 := cam.RayAtUV(5.5, 5.5, 11, 11)
175- if c.Dir.Sub(c2.Dir).Len() > 1e-12 {
176- t.Errorf("RayAt != RayAtUV center: %v vs %v", c.Dir, c2.Dir)
177- }
178- // A sample in the top-left quadrant points up and left.
179- tl := cam.RayAtUV(0.25, 0.25, 11, 11)
180- if tl.Dir.X >= 0 || tl.Dir.Y <= 0 {
181- t.Errorf("top-left sample = %v, want X<0, Y>0", tl.Dir)
182- }
183- // A sample in the bottom-right quadrant points down and right.
184- br := cam.RayAtUV(10.75, 10.75, 11, 11)
185- if br.Dir.X <= 0 || br.Dir.Y >= 0 {
186- t.Errorf("bottom-right sample = %v, want X>0, Y<0", br.Dir)
187- }
188-}
189-
190-func TestSupersampling(t *testing.T) {
191- // A scene with a hard edge: a red sphere against a mid-gray
192- // background. With 1 sample every pixel is pure red or pure gray;
193- // with 2x2 samples the silhouette pixels must be a blend.
194- build := func() *Scene {
195- s := NewScene()
196- s.AddObject(Sphere{Center: Vec3{0, 0, 0}, Radius: 1},
197- Material{Diffuse: Color{1, 0, 0}, Ambient: 1})
198- s.Background = Color{0.5, 0.5, 0.5}
199- return s
200- }
201- cam := NewCamera(Vec3{0, 0, 5})
202-
203- // Count pixels in row 20 (where the sphere's silhouette has a steep
204- // slope, so a pixel's 2x2 sub-samples straddle the edge) whose green
205- // channel is strictly between the sphere (pure red, G=0) and the
206- // background (gray, G≈186 after gamma): a "mixed" pixel is an
207- // anti-aliased edge.
208- mixed := func(img *Image) int {
209- var n int
210- for x := 0; x < img.Width; x++ {
211- i := (20*img.Width + x) * 3
212- if img.Pix[i+1] > 5 && img.Pix[i+1] < 181 {
213- n++
214- }
215- }
216- return n
217- }
218-
219- r1 := Renderer{Scene: build(), Width: 64, Height: 48, Samples: 1}
220- if m := mixed(r1.Render(cam)); m > 0 {
221- t.Errorf("1-sample render has %d mixed pixels, want 0", m)
222- }
223-
224- r2 := Renderer{Scene: build(), Width: 64, Height: 48, Samples: 2}
225- if m := mixed(r2.Render(cam)); m == 0 {
226- t.Error("2x2 render has no mixed pixels: edge not anti-aliased")
227- }
228-}
229-
230-func TestPathTracer(t *testing.T) {
231- // A room: white floor, a sphere, and an emissive panel above.
232- // Path tracing must light the floor from the panel, and the floor
233- // behind the sphere (toward the camera) must be in shadow.
234- build := func() *Scene {
235- s := NewScene()
236- s.Background = Black
237- s.AddObject(NewPlaneXY(0), Material{Diffuse: White})
238- s.AddObject(Sphere{Center: Vec3{0, 1, 0}, Radius: 1}, Material{Diffuse: White})
239- // A downward-facing emissive panel.
240- panel := Transform{
241- Object: Box{Min: Vec3{-2, 0, -2}, Max: Vec3{2, 0.1, 2}},
242- Matrix: Translate(0, 8, 0),
243- }
244- s.AddObject(panel, LightMaterial(White, 12))
245- return s
246- }
247- cam := NewCamera(Vec3{0, 2, 8})
248- p := PathTracer{Scene: build(), Width: 64, Height: 48, Samples: 8}
249- img := p.Render(cam)
250-
251- // The floor occupies the bottom half of the image. It must be
252- // lit (not black) and must have a bright region (direct light from
253- // the panel) and a dark region (the sphere's shadow).
254- var min, max byte
255- for y := 24; y < 48; y++ {
256- for x := 0; x < 64; x++ {
257- v := img.Pix[(y*64+x)*3]
258- if x == 0 || v < min {
259- min = v
260- }
261- if x == 0 || v > max {
262- max = v
263- }
264- }
265- }
266- if max < 40 {
267- t.Errorf("floor not lit by the panel: max = %d", max)
268- }
269- if float64(max) < 2*float64(min+1) {
270- t.Errorf("no shadow visible: min = %d, max = %d", min, max)
271- }
272-
273- // Determinism: the same configuration must give the same image.
274- img2 := p.Render(cam)
275- if !bytes.Equal(img.Pix, img2.Pix) {
276- t.Error("two renders of the same scene differ")
277- }
278-}
279-
280-func TestRenderSmoke(t *testing.T) {
281- s := NewScene()
282- s.AddObject(Sphere{Center: Vec3{0, 1, 0}, Radius: 1},
283- Material{Diffuse: Red, Specular: White, Shininess: 30, Ambient: 0.1})
284- s.AddObject(NewPlaneXY(0), Material{Diffuse: Gray, Ambient: 0.3})
285- s.Lights = []Light{NewLight(Vec3{4, 8, 6}, White)}
286-
287- cam := NewCamera(Vec3{0, 2, 8})
288- r := Renderer{Scene: s, Width: 64, Height: 48}
289- img := r.Render(cam)
290- if img.Width != 64 || img.Height != 48 {
291- t.Fatalf("image size = %dx%d", img.Width, img.Height)
292- }
293- // The center pixel should hit the red sphere: red channel dominant.
294- i := (24*64 + 32) * 3
295- if img.Pix[i] < 60 {
296- t.Errorf("center pixel too dark: %v", img.Pix[i:i+3])
297- }
298- if img.Pix[i+1] > img.Pix[i] || img.Pix[i+2] > img.Pix[i] {
299- t.Errorf("center pixel not red-dominant: %v", img.Pix[i:i+3])
300- }
301-}
302-
303-func TestSavePNG(t *testing.T) {
304- img := NewImage(4, 3)
305- for y := 0; y < 3; y++ {
306- for x := 0; x < 4; x++ {
307- img.Set(x, y, Color{float64(x) / 3, float64(y) / 2, 1})
308- }
309- }
310- path := t.TempDir() + "/test.png"
311- if err := img.SavePNG(path); err != nil {
312- t.Fatal(err)
313- }
314- data, err := os.ReadFile(path)
315- if err != nil {
316- t.Fatal(err)
317- }
318- // PNG signature.
319- sig := []byte{137, 80, 78, 71, 13, 10, 26, 10}
320- if !bytes.Equal(data[:8], sig) {
321- t.Fatalf("bad signature: %v", data[:8])
322- }
323- // IHDR must say 4x3, 8-bit truecolor.
324- if binary.BigEndian.Uint32(data[16:20]) != 4 || binary.BigEndian.Uint32(data[20:24]) != 3 {
325- t.Errorf("IHDR size wrong: %v", data[16:24])
326- }
327- if data[24] != 8 || data[25] != 2 {
328- t.Errorf("IHDR depth/type wrong: %d %d", data[24], data[25])
329- }
330- // The file must end with an IEND chunk: length 0, "IEND", CRC AE426082.
331- if !bytes.Equal(data[len(data)-8:len(data)-4], []byte("IEND")) ||
332- !bytes.Equal(data[len(data)-4:], []byte{0xAE, 0x42, 0x60, 0x82}) ||
333- !bytes.Equal(data[len(data)-12:len(data)-8], []byte{0, 0, 0, 0}) {
334- t.Errorf("missing IEND trailer: %v", data[len(data)-12:])
335- }
336-}
deleted demos/qwen-style/workspace/renderer.go +0 -256
deleted file mode 100644
@@ -1,256 +0,0 @@
1-package povray
2-
3-import (
4- "fmt"
5- "math"
6- "os"
7- "sync"
8-)
9-
10-// Scene bundles everything a render needs.
11-type Scene struct {
12- // Objects are the geometry; pass a Group to combine several.
13- Objects []Object
14- // Materials are looked up by index; Objects[i] uses Materials[i].
15- Materials []Material
16- // Lights light the scene.
17- Lights []Light
18- // Background is the color of empty space.
19- Background Color
20- // Gamma applied at the end (2.2 is a good default).
21- Gamma float64
22- // MaxDepth limits reflection/refraction bounces.
23- MaxDepth int
24-}
25-
26-// NewScene makes a scene with a white background, gamma 2.2 and 8 bounces.
27-func NewScene() *Scene {
28- return &Scene{Background: White, Gamma: 2.2, MaxDepth: 8}
29-}
30-
31-// AddObject registers an object and its material.
32-func (s *Scene) AddObject(o Object, m Material) {
33- s.Objects = append(s.Objects, o)
34- s.Materials = append(s.Materials, m)
35-}
36-
37-// AddObjects registers several objects sharing one material.
38-func (s *Scene) AddObjects(o []Object, m Material) {
39- for _, x := range o {
40- s.AddObject(x, m)
41- }
42-}
43-
44-// Renderer renders scenes.
45-type Renderer struct {
46- Scene *Scene
47- // Width and Height of the image in pixels.
48- Width, Height int
49- // Threads is the number of worker goroutines (0 = GOMAXPROCS).
50- Threads int
51- // Samples is the number of rays per pixel, arranged in a grid
52- // (1 = no anti-aliasing; 2 means 2x2 = 4 rays, 3 means 3x3 = 9).
53- Samples int
54-}
55-
56-// Render renders the scene from the camera and returns the image.
57-func (r *Renderer) Render(cam Camera) *Image {
58- s := r.Scene
59- img := NewImage(r.Width, r.Height)
60- if s.Gamma <= 0 {
61- s.Gamma = 2.2
62- }
63- if s.MaxDepth <= 0 {
64- s.MaxDepth = 8
65- }
66- threads := r.Threads
67- if threads <= 0 {
68- threads = 4
69- }
70- samples := r.Samples
71- if samples < 1 {
72- samples = 1
73- }
74-
75- var wg sync.WaitGroup
76- chunks := threads
77- rowsPer := (r.Height + chunks - 1) / chunks
78- for c := 0; c < chunks; c++ {
79- y0 := c * rowsPer
80- y1 := y0 + rowsPer
81- if y1 > r.Height {
82- y1 = r.Height
83- }
84- if y0 >= y1 {
85- break
86- }
87- wg.Add(1)
88- go func(y0, y1 int) {
89- defer wg.Done()
90- for y := y0; y < y1; y++ {
91- for x := 0; x < r.Width; x++ {
92- var col Color
93- for sy := 0; sy < samples; sy++ {
94- for sx := 0; sx < samples; sx++ {
95- u := float64(x) + (float64(sx)+0.5)/float64(samples)
96- v := float64(y) + (float64(sy)+0.5)/float64(samples)
97- col = col.Add(r.trace(cam.RayAtUV(u, v, r.Width, r.Height), 0))
98- }
99- }
100- img.Set(x, y, col.Scale(1.0/float64(samples*samples)))
101- }
102- }
103- }(y0, y1)
104- }
105- wg.Wait()
106- return img
107-}
108-
109-// trace follows a ray until it leaves the scene or the bounce budget.
110-func (r *Renderer) trace(ray Ray, depth int) Color {
111- s := r.Scene
112- if depth > s.MaxDepth {
113- return Black
114- }
115-
116- idx, hit := closestHit(s.Objects, ray)
117- if idx < 0 {
118- return s.Background
119- }
120- hit.Flip(ray)
121- mat := s.Materials[idx]
122-
123- col := r.shade(ray, hit, mat, depth)
124-
125- // Reflection: mirror the ray, keep the surface color as a tint.
126- if mat.Reflectivity > 0 {
127- reflect := ray.Dir.Sub(hit.Normal.Scale(2 * ray.Dir.Dot(hit.Normal))).Norm()
128- rr := Ray{Origin: hit.Point.Add(hit.Normal.Scale(1e-4)), Dir: reflect}
129- col = col.Add(r.trace(rr, depth+1).Mul(mat.Diffuse).Scale(mat.Reflectivity))
130- }
131-
132- // Refraction: bend the ray through the surface.
133- if mat.Transparency > 0 {
134- ior := 1.0
135- if mat.IOR > 0 {
136- ior = mat.IOR
137- }
138- refr, ok := refract(ray.Dir, hit.Normal, 1/ior)
139- if ok {
140- tr := Ray{Origin: hit.Point.Add(hit.Normal.Scale(1e-4)), Dir: refr}
141- col = col.Add(r.trace(tr, depth+1).Scale(mat.Transparency))
142- }
143- }
144- return col
145-}
146-
147-// shade computes ambient + diffuse + specular with shadows for one hit.
148-func (r *Renderer) shade(ray Ray, hit Hit, mat Material, depth int) Color {
149- s := r.Scene
150- col := mat.Diffuse.Scale(mat.Ambient)
151- for _, l := range s.Lights {
152- toLight := l.Position.Sub(hit.Point)
153- dist := toLight.Len()
154- if dist < 1e-6 {
155- continue
156- }
157- dir := toLight.Scale(1 / dist)
158- // Shadow: is the light occluded?
159- if occluded(s.Objects, s.Materials, Ray{Origin: hit.Point.Add(hit.Normal.Scale(1e-4)), Dir: dir}, dist) {
160- continue
161- }
162- // Inverse-square falloff, clamped so close lights don't blow out.
163- fall := 1.0 / math.Max(1.0, dist*dist*0.1)
164- lc := l.Color.Scale(l.Intensity * fall)
165-
166- diff := math.Max(0, hit.Normal.Dot(dir))
167- col = col.Add(mat.Diffuse.Mul(lc).Scale(diff))
168-
169- if mat.Shininess > 0 && diff > 0 {
170- half := dir.Add(ray.Dir.Neg()).Norm()
171- spec := math.Max(0, hit.Normal.Dot(half))
172- col = col.Add(mat.Specular.Mul(lc).Scale(math.Pow(spec, mat.Shininess)))
173- }
174- }
175- return col
176-}
177-
178-// closestHit finds the nearest object hit for the ray.
179-func closestHit(objects []Object, ray Ray) (int, Hit) {
180- bestIdx, best := -1, Hit{}
181- for i, o := range objects {
182- if h, ok := o.Intersect(ray); ok && (bestIdx < 0 || h.Dist < best.Dist) {
183- bestIdx, best = i, h
184- }
185- }
186- return bestIdx, best
187-}
188-
189-// occluded reports whether an opaque object is hit before maxDist.
190-func occluded(objects []Object, mats []Material, ray Ray, maxDist float64) bool {
191- for i, o := range objects {
192- if mats[i].Transparency > 0.5 {
193- continue
194- }
195- if h, ok := o.Intersect(ray); ok && h.Dist < maxDist {
196- return true
197- }
198- }
199- return false
200-}
201-
202-// refract bends dir across a surface with normal n and ratio ior
203-// (1/ior when exiting the medium). Returns (direction, ok).
204-func refract(dir, n Vec3, ior float64) (Vec3, bool) {
205- cosI := -dir.Dot(n)
206- sin2T := ior * ior * (1 - cosI*cosI)
207- if sin2T > 1 {
208- return Vec3{}, false // total internal reflection
209- }
210- cosT := math.Sqrt(1 - sin2T)
211- return dir.Scale(ior).Sub(n.Scale(ior*cosT - cosI)).Norm(), true
212-}
213-
214-// Image is a plain RGB image.
215-type Image struct {
216- Width, Height int
217- Pix []byte // RGB, row-major, 3 bytes per pixel
218-}
219-
220-// NewImage allocates a black image.
221-func NewImage(w, h int) *Image {
222- return &Image{Width: w, Height: h, Pix: make([]byte, w*h*3)}
223-}
224-
225-// Set stores a color at (x, y) with gamma correction and clipping.
226-func (img *Image) Set(x, y int, c Color) {
227- i := (y*img.Width + x) * 3
228- img.Pix[i] = toByte(c.R)
229- img.Pix[i+1] = toByte(c.G)
230- img.Pix[i+2] = toByte(c.B)
231-}
232-
233-func toByte(v float64) byte {
234- if v < 0 {
235- v = 0
236- }
237- if v > 1 {
238- v = 1
239- }
240- return byte(255 * math.Pow(v, 1/2.2))
241-}
242-
243-// SavePPM writes the image as a binary PPM (P6) file.
244-func (img *Image) SavePPM(path string) error {
245- f, err := os.Create(path)
246- if err != nil {
247- return err
248- }
249- defer f.Close()
250- _, err = fmt.Fprintf(f, "P6\n%d %d\n255\n", img.Width, img.Height)
251- if err != nil {
252- return err
253- }
254- _, err = f.Write(img.Pix)
255- return err
256-}
deleted file mode 100644
@@ -1,256 +0,0 @@
1-package povray
2-
3-import (
4- "fmt"
5- "math"
6- "os"
7- "sync"
8-)
9-
10-// Scene bundles everything a render needs.
11-type Scene struct {
12- // Objects are the geometry; pass a Group to combine several.
13- Objects []Object
14- // Materials are looked up by index; Objects[i] uses Materials[i].
15- Materials []Material
16- // Lights light the scene.
17- Lights []Light
18- // Background is the color of empty space.
19- Background Color
20- // Gamma applied at the end (2.2 is a good default).
21- Gamma float64
22- // MaxDepth limits reflection/refraction bounces.
23- MaxDepth int
24-}
25-
26-// NewScene makes a scene with a white background, gamma 2.2 and 8 bounces.
27-func NewScene() *Scene {
28- return &Scene{Background: White, Gamma: 2.2, MaxDepth: 8}
29-}
30-
31-// AddObject registers an object and its material.
32-func (s *Scene) AddObject(o Object, m Material) {
33- s.Objects = append(s.Objects, o)
34- s.Materials = append(s.Materials, m)
35-}
36-
37-// AddObjects registers several objects sharing one material.
38-func (s *Scene) AddObjects(o []Object, m Material) {
39- for _, x := range o {
40- s.AddObject(x, m)
41- }
42-}
43-
44-// Renderer renders scenes.
45-type Renderer struct {
46- Scene *Scene
47- // Width and Height of the image in pixels.
48- Width, Height int
49- // Threads is the number of worker goroutines (0 = GOMAXPROCS).
50- Threads int
51- // Samples is the number of rays per pixel, arranged in a grid
52- // (1 = no anti-aliasing; 2 means 2x2 = 4 rays, 3 means 3x3 = 9).
53- Samples int
54-}
55-
56-// Render renders the scene from the camera and returns the image.
57-func (r *Renderer) Render(cam Camera) *Image {
58- s := r.Scene
59- img := NewImage(r.Width, r.Height)
60- if s.Gamma <= 0 {
61- s.Gamma = 2.2
62- }
63- if s.MaxDepth <= 0 {
64- s.MaxDepth = 8
65- }
66- threads := r.Threads
67- if threads <= 0 {
68- threads = 4
69- }
70- samples := r.Samples
71- if samples < 1 {
72- samples = 1
73- }
74-
75- var wg sync.WaitGroup
76- chunks := threads
77- rowsPer := (r.Height + chunks - 1) / chunks
78- for c := 0; c < chunks; c++ {
79- y0 := c * rowsPer
80- y1 := y0 + rowsPer
81- if y1 > r.Height {
82- y1 = r.Height
83- }
84- if y0 >= y1 {
85- break
86- }
87- wg.Add(1)
88- go func(y0, y1 int) {
89- defer wg.Done()
90- for y := y0; y < y1; y++ {
91- for x := 0; x < r.Width; x++ {
92- var col Color
93- for sy := 0; sy < samples; sy++ {
94- for sx := 0; sx < samples; sx++ {
95- u := float64(x) + (float64(sx)+0.5)/float64(samples)
96- v := float64(y) + (float64(sy)+0.5)/float64(samples)
97- col = col.Add(r.trace(cam.RayAtUV(u, v, r.Width, r.Height), 0))
98- }
99- }
100- img.Set(x, y, col.Scale(1.0/float64(samples*samples)))
101- }
102- }
103- }(y0, y1)
104- }
105- wg.Wait()
106- return img
107-}
108-
109-// trace follows a ray until it leaves the scene or the bounce budget.
110-func (r *Renderer) trace(ray Ray, depth int) Color {
111- s := r.Scene
112- if depth > s.MaxDepth {
113- return Black
114- }
115-
116- idx, hit := closestHit(s.Objects, ray)
117- if idx < 0 {
118- return s.Background
119- }
120- hit.Flip(ray)
121- mat := s.Materials[idx]
122-
123- col := r.shade(ray, hit, mat, depth)
124-
125- // Reflection: mirror the ray, keep the surface color as a tint.
126- if mat.Reflectivity > 0 {
127- reflect := ray.Dir.Sub(hit.Normal.Scale(2 * ray.Dir.Dot(hit.Normal))).Norm()
128- rr := Ray{Origin: hit.Point.Add(hit.Normal.Scale(1e-4)), Dir: reflect}
129- col = col.Add(r.trace(rr, depth+1).Mul(mat.Diffuse).Scale(mat.Reflectivity))
130- }
131-
132- // Refraction: bend the ray through the surface.
133- if mat.Transparency > 0 {
134- ior := 1.0
135- if mat.IOR > 0 {
136- ior = mat.IOR
137- }
138- refr, ok := refract(ray.Dir, hit.Normal, 1/ior)
139- if ok {
140- tr := Ray{Origin: hit.Point.Add(hit.Normal.Scale(1e-4)), Dir: refr}
141- col = col.Add(r.trace(tr, depth+1).Scale(mat.Transparency))
142- }
143- }
144- return col
145-}
146-
147-// shade computes ambient + diffuse + specular with shadows for one hit.
148-func (r *Renderer) shade(ray Ray, hit Hit, mat Material, depth int) Color {
149- s := r.Scene
150- col := mat.Diffuse.Scale(mat.Ambient)
151- for _, l := range s.Lights {
152- toLight := l.Position.Sub(hit.Point)
153- dist := toLight.Len()
154- if dist < 1e-6 {
155- continue
156- }
157- dir := toLight.Scale(1 / dist)
158- // Shadow: is the light occluded?
159- if occluded(s.Objects, s.Materials, Ray{Origin: hit.Point.Add(hit.Normal.Scale(1e-4)), Dir: dir}, dist) {
160- continue
161- }
162- // Inverse-square falloff, clamped so close lights don't blow out.
163- fall := 1.0 / math.Max(1.0, dist*dist*0.1)
164- lc := l.Color.Scale(l.Intensity * fall)
165-
166- diff := math.Max(0, hit.Normal.Dot(dir))
167- col = col.Add(mat.Diffuse.Mul(lc).Scale(diff))
168-
169- if mat.Shininess > 0 && diff > 0 {
170- half := dir.Add(ray.Dir.Neg()).Norm()
171- spec := math.Max(0, hit.Normal.Dot(half))
172- col = col.Add(mat.Specular.Mul(lc).Scale(math.Pow(spec, mat.Shininess)))
173- }
174- }
175- return col
176-}
177-
178-// closestHit finds the nearest object hit for the ray.
179-func closestHit(objects []Object, ray Ray) (int, Hit) {
180- bestIdx, best := -1, Hit{}
181- for i, o := range objects {
182- if h, ok := o.Intersect(ray); ok && (bestIdx < 0 || h.Dist < best.Dist) {
183- bestIdx, best = i, h
184- }
185- }
186- return bestIdx, best
187-}
188-
189-// occluded reports whether an opaque object is hit before maxDist.
190-func occluded(objects []Object, mats []Material, ray Ray, maxDist float64) bool {
191- for i, o := range objects {
192- if mats[i].Transparency > 0.5 {
193- continue
194- }
195- if h, ok := o.Intersect(ray); ok && h.Dist < maxDist {
196- return true
197- }
198- }
199- return false
200-}
201-
202-// refract bends dir across a surface with normal n and ratio ior
203-// (1/ior when exiting the medium). Returns (direction, ok).
204-func refract(dir, n Vec3, ior float64) (Vec3, bool) {
205- cosI := -dir.Dot(n)
206- sin2T := ior * ior * (1 - cosI*cosI)
207- if sin2T > 1 {
208- return Vec3{}, false // total internal reflection
209- }
210- cosT := math.Sqrt(1 - sin2T)
211- return dir.Scale(ior).Sub(n.Scale(ior*cosT - cosI)).Norm(), true
212-}
213-
214-// Image is a plain RGB image.
215-type Image struct {
216- Width, Height int
217- Pix []byte // RGB, row-major, 3 bytes per pixel
218-}
219-
220-// NewImage allocates a black image.
221-func NewImage(w, h int) *Image {
222- return &Image{Width: w, Height: h, Pix: make([]byte, w*h*3)}
223-}
224-
225-// Set stores a color at (x, y) with gamma correction and clipping.
226-func (img *Image) Set(x, y int, c Color) {
227- i := (y*img.Width + x) * 3
228- img.Pix[i] = toByte(c.R)
229- img.Pix[i+1] = toByte(c.G)
230- img.Pix[i+2] = toByte(c.B)
231-}
232-
233-func toByte(v float64) byte {
234- if v < 0 {
235- v = 0
236- }
237- if v > 1 {
238- v = 1
239- }
240- return byte(255 * math.Pow(v, 1/2.2))
241-}
242-
243-// SavePPM writes the image as a binary PPM (P6) file.
244-func (img *Image) SavePPM(path string) error {
245- f, err := os.Create(path)
246- if err != nil {
247- return err
248- }
249- defer f.Close()
250- _, err = fmt.Fprintf(f, "P6\n%d %d\n255\n", img.Width, img.Height)
251- if err != nil {
252- return err
253- }
254- _, err = f.Write(img.Pix)
255- return err
256-}
deleted demos/qwen-style/workspace/transform.go +0 -181
deleted file mode 100644
@@ -1,181 +0,0 @@
1-package povray
2-
3-import "math"
4-
5-// Mat4 is a 4x4 transformation matrix, row-major (m[row][col]).
6-// It maps a world point p to m.TransformPoint(p).
7-type Mat4 [4][4]float64
8-
9-// Identity returns the 4x4 identity matrix.
10-func Identity() Mat4 {
11- var m Mat4
12- for i := range m {
13- m[i][i] = 1
14- }
15- return m
16-}
17-
18-// Multiply returns the product m * o, i.e. a transformation that
19-// applies o first, then m.
20-func (m Mat4) Multiply(o Mat4) Mat4 {
21- var r Mat4
22- for i := 0; i < 4; i++ {
23- for j := 0; j < 4; j++ {
24- var s float64
25- for k := 0; k < 4; k++ {
26- s += m[i][k] * o[k][j]
27- }
28- r[i][j] = s
29- }
30- }
31- return r
32-}
33-
34-// Translate returns a translation matrix.
35-func Translate(x, y, z float64) Mat4 {
36- m := Identity()
37- m[0][3], m[1][3], m[2][3] = x, y, z
38- return m
39-}
40-
41-// Scale returns a (possibly non-uniform) scale matrix.
42-func Scale(x, y, z float64) Mat4 {
43- m := Identity()
44- m[0][0], m[1][1], m[2][2] = x, y, z
45- return m
46-}
47-
48-// RotateX returns a rotation of angle a (radians) around the X axis.
49-func RotateX(a float64) Mat4 {
50- c, s := math.Cos(a), math.Sin(a)
51- return Mat4{
52- {1, 0, 0, 0},
53- {0, c, -s, 0},
54- {0, s, c, 0},
55- {0, 0, 0, 1},
56- }
57-}
58-
59-// RotateY returns a rotation of angle a (radians) around the Y axis.
60-func RotateY(a float64) Mat4 {
61- c, s := math.Cos(a), math.Sin(a)
62- return Mat4{
63- {c, 0, s, 0},
64- {0, 1, 0, 0},
65- {-s, 0, c, 0},
66- {0, 0, 0, 1},
67- }
68-}
69-
70-// RotateZ returns a rotation of angle a (radians) around the Z axis.
71-func RotateZ(a float64) Mat4 {
72- c, s := math.Cos(a), math.Sin(a)
73- return Mat4{
74- {c, -s, 0, 0},
75- {s, c, 0, 0},
76- {0, 0, 1, 0},
77- {0, 0, 0, 1},
78- }
79-}
80-
81-// TransformPoint applies the matrix to a point (translation included).
82-func (m Mat4) TransformPoint(p Vec3) Vec3 {
83- return Vec3{
84- m[0][0]*p.X + m[0][1]*p.Y + m[0][2]*p.Z + m[0][3],
85- m[1][0]*p.X + m[1][1]*p.Y + m[1][2]*p.Z + m[1][3],
86- m[2][0]*p.X + m[2][1]*p.Y + m[2][2]*p.Z + m[2][3],
87- }
88-}
89-
90-// TransformDir applies the matrix to a direction (no translation).
91-// The result is not normalized; the intersection math does not require it.
92-func (m Mat4) TransformDir(d Vec3) Vec3 {
93- return Vec3{
94- m[0][0]*d.X + m[0][1]*d.Y + m[0][2]*d.Z,
95- m[1][0]*d.X + m[1][1]*d.Y + m[1][2]*d.Z,
96- m[2][0]*d.X + m[2][1]*d.Y + m[2][2]*d.Z,
97- }
98-}
99-
100-// Inverse returns the inverse matrix. ok is false if the matrix is
101-// singular.
102-func (m Mat4) Inverse() (Mat4, bool) {
103- // Gauss-Jordan on the augmented system [m | I].
104- a := make([][]float64, 4)
105- for i := range a {
106- a[i] = make([]float64, 8)
107- copy(a[i][:4], m[i][:])
108- a[i][4+i] = 1
109- }
110- for col := 0; col < 4; col++ {
111- piv := col
112- for r := col + 1; r < 4; r++ {
113- if math.Abs(a[r][col]) > math.Abs(a[piv][col]) {
114- piv = r
115- }
116- }
117- if math.Abs(a[piv][col]) < 1e-12 {
118- return Mat4{}, false
119- }
120- a[col], a[piv] = a[piv], a[col]
121- pv := a[col][col]
122- for c := col; c < 8; c++ {
123- a[col][c] /= pv
124- }
125- for r := 0; r < 4; r++ {
126- if r == col || a[r][col] == 0 {
127- continue
128- }
129- f := a[r][col]
130- for c := col; c < 8; c++ {
131- a[r][c] -= f * a[col][c]
132- }
133- }
134- }
135- var inv Mat4
136- for i := 0; i < 4; i++ {
137- copy(inv[i][:], a[i][4:])
138- }
139- return inv, true
140-}
141-
142-// TransformNormal maps a surface normal into world space using the
143-// inverse-transpose of the linear part (correct under non-uniform
144-// scale), and returns it normalized.
145-func (m Mat4) TransformNormal(n Vec3) Vec3 {
146- inv, ok := m.Inverse()
147- if !ok {
148- return n
149- }
150- return Vec3{
151- inv[0][0]*n.X + inv[1][0]*n.Y + inv[2][0]*n.Z,
152- inv[0][1]*n.X + inv[1][1]*n.Y + inv[2][1]*n.Z,
153- inv[0][2]*n.X + inv[1][2]*n.Y + inv[2][2]*n.Z,
154- }.Norm()
155-}
156-
157-// Transform is an object wrapped in a 4x4 matrix: the ray is mapped
158-// into the object's local space, intersected there, and the hit is
159-// mapped back to world space.
160-type Transform struct {
161- Object Object
162- Matrix Mat4
163-}
164-
165-func (t Transform) Intersect(r Ray) (Hit, bool) {
166- inv, ok := t.Matrix.Inverse()
167- if !ok {
168- return Hit{}, false
169- }
170- lr := Ray{Origin: inv.TransformPoint(r.Origin), Dir: inv.TransformDir(r.Dir)}
171- h, ok := t.Object.Intersect(lr)
172- if !ok {
173- return Hit{}, false
174- }
175- p := t.Matrix.TransformPoint(h.Point)
176- return Hit{
177- Point: p,
178- Dist: p.Sub(r.Origin).Len(),
179- Normal: t.Matrix.TransformNormal(h.Normal),
180- }, true
181-}
deleted file mode 100644
@@ -1,181 +0,0 @@
1-package povray
2-
3-import "math"
4-
5-// Mat4 is a 4x4 transformation matrix, row-major (m[row][col]).
6-// It maps a world point p to m.TransformPoint(p).
7-type Mat4 [4][4]float64
8-
9-// Identity returns the 4x4 identity matrix.
10-func Identity() Mat4 {
11- var m Mat4
12- for i := range m {
13- m[i][i] = 1
14- }
15- return m
16-}
17-
18-// Multiply returns the product m * o, i.e. a transformation that
19-// applies o first, then m.
20-func (m Mat4) Multiply(o Mat4) Mat4 {
21- var r Mat4
22- for i := 0; i < 4; i++ {
23- for j := 0; j < 4; j++ {
24- var s float64
25- for k := 0; k < 4; k++ {
26- s += m[i][k] * o[k][j]
27- }
28- r[i][j] = s
29- }
30- }
31- return r
32-}
33-
34-// Translate returns a translation matrix.
35-func Translate(x, y, z float64) Mat4 {
36- m := Identity()
37- m[0][3], m[1][3], m[2][3] = x, y, z
38- return m
39-}
40-
41-// Scale returns a (possibly non-uniform) scale matrix.
42-func Scale(x, y, z float64) Mat4 {
43- m := Identity()
44- m[0][0], m[1][1], m[2][2] = x, y, z
45- return m
46-}
47-
48-// RotateX returns a rotation of angle a (radians) around the X axis.
49-func RotateX(a float64) Mat4 {
50- c, s := math.Cos(a), math.Sin(a)
51- return Mat4{
52- {1, 0, 0, 0},
53- {0, c, -s, 0},
54- {0, s, c, 0},
55- {0, 0, 0, 1},
56- }
57-}
58-
59-// RotateY returns a rotation of angle a (radians) around the Y axis.
60-func RotateY(a float64) Mat4 {
61- c, s := math.Cos(a), math.Sin(a)
62- return Mat4{
63- {c, 0, s, 0},
64- {0, 1, 0, 0},
65- {-s, 0, c, 0},
66- {0, 0, 0, 1},
67- }
68-}
69-
70-// RotateZ returns a rotation of angle a (radians) around the Z axis.
71-func RotateZ(a float64) Mat4 {
72- c, s := math.Cos(a), math.Sin(a)
73- return Mat4{
74- {c, -s, 0, 0},
75- {s, c, 0, 0},
76- {0, 0, 1, 0},
77- {0, 0, 0, 1},
78- }
79-}
80-
81-// TransformPoint applies the matrix to a point (translation included).
82-func (m Mat4) TransformPoint(p Vec3) Vec3 {
83- return Vec3{
84- m[0][0]*p.X + m[0][1]*p.Y + m[0][2]*p.Z + m[0][3],
85- m[1][0]*p.X + m[1][1]*p.Y + m[1][2]*p.Z + m[1][3],
86- m[2][0]*p.X + m[2][1]*p.Y + m[2][2]*p.Z + m[2][3],
87- }
88-}
89-
90-// TransformDir applies the matrix to a direction (no translation).
91-// The result is not normalized; the intersection math does not require it.
92-func (m Mat4) TransformDir(d Vec3) Vec3 {
93- return Vec3{
94- m[0][0]*d.X + m[0][1]*d.Y + m[0][2]*d.Z,
95- m[1][0]*d.X + m[1][1]*d.Y + m[1][2]*d.Z,
96- m[2][0]*d.X + m[2][1]*d.Y + m[2][2]*d.Z,
97- }
98-}
99-
100-// Inverse returns the inverse matrix. ok is false if the matrix is
101-// singular.
102-func (m Mat4) Inverse() (Mat4, bool) {
103- // Gauss-Jordan on the augmented system [m | I].
104- a := make([][]float64, 4)
105- for i := range a {
106- a[i] = make([]float64, 8)
107- copy(a[i][:4], m[i][:])
108- a[i][4+i] = 1
109- }
110- for col := 0; col < 4; col++ {
111- piv := col
112- for r := col + 1; r < 4; r++ {
113- if math.Abs(a[r][col]) > math.Abs(a[piv][col]) {
114- piv = r
115- }
116- }
117- if math.Abs(a[piv][col]) < 1e-12 {
118- return Mat4{}, false
119- }
120- a[col], a[piv] = a[piv], a[col]
121- pv := a[col][col]
122- for c := col; c < 8; c++ {
123- a[col][c] /= pv
124- }
125- for r := 0; r < 4; r++ {
126- if r == col || a[r][col] == 0 {
127- continue
128- }
129- f := a[r][col]
130- for c := col; c < 8; c++ {
131- a[r][c] -= f * a[col][c]
132- }
133- }
134- }
135- var inv Mat4
136- for i := 0; i < 4; i++ {
137- copy(inv[i][:], a[i][4:])
138- }
139- return inv, true
140-}
141-
142-// TransformNormal maps a surface normal into world space using the
143-// inverse-transpose of the linear part (correct under non-uniform
144-// scale), and returns it normalized.
145-func (m Mat4) TransformNormal(n Vec3) Vec3 {
146- inv, ok := m.Inverse()
147- if !ok {
148- return n
149- }
150- return Vec3{
151- inv[0][0]*n.X + inv[1][0]*n.Y + inv[2][0]*n.Z,
152- inv[0][1]*n.X + inv[1][1]*n.Y + inv[2][1]*n.Z,
153- inv[0][2]*n.X + inv[1][2]*n.Y + inv[2][2]*n.Z,
154- }.Norm()
155-}
156-
157-// Transform is an object wrapped in a 4x4 matrix: the ray is mapped
158-// into the object's local space, intersected there, and the hit is
159-// mapped back to world space.
160-type Transform struct {
161- Object Object
162- Matrix Mat4
163-}
164-
165-func (t Transform) Intersect(r Ray) (Hit, bool) {
166- inv, ok := t.Matrix.Inverse()
167- if !ok {
168- return Hit{}, false
169- }
170- lr := Ray{Origin: inv.TransformPoint(r.Origin), Dir: inv.TransformDir(r.Dir)}
171- h, ok := t.Object.Intersect(lr)
172- if !ok {
173- return Hit{}, false
174- }
175- p := t.Matrix.TransformPoint(h.Point)
176- return Hit{
177- Point: p,
178- Dist: p.Sub(r.Origin).Len(),
179- Normal: t.Matrix.TransformNormal(h.Normal),
180- }, true
181-}
deleted demos/qwen-style/workspace/vector.go +0 -54
deleted file mode 100644
@@ -1,54 +0,0 @@
1-package povray
2-
3-import "math"
4-
5-// Vec3 is a 3D vector.
6-type Vec3 struct {
7- X, Y, Z float64
8-}
9-
10-func (v Vec3) Add(o Vec3) Vec3 { return Vec3{v.X + o.X, v.Y + o.Y, v.Z + o.Z} }
11-func (v Vec3) Sub(o Vec3) Vec3 { return Vec3{v.X - o.X, v.Y - o.Y, v.Z - o.Z} }
12-func (v Vec3) Scale(s float64) Vec3 {
13- return Vec3{v.X * s, v.Y * s, v.Z * s}
14-}
15-func (v Vec3) Mul(o Vec3) Vec3 { return Vec3{v.X * o.X, v.Y * o.Y, v.Z * o.Z} }
16-
17-func (v Vec3) Dot(o Vec3) float64 { return v.X*o.X + v.Y*o.Y + v.Z*o.Z }
18-func (v Vec3) Cross(o Vec3) Vec3 {
19- return Vec3{
20- v.Y*o.Z - v.Z*o.Y,
21- v.Z*o.X - v.X*o.Z,
22- v.X*o.Y - v.Y*o.X,
23- }
24-}
25-
26-func (v Vec3) Len() float64 { return math.Sqrt(v.Dot(v)) }
27-func (v Vec3) Norm() Vec3 {
28- l := v.Len()
29- if l == 0 {
30- return Vec3{}
31- }
32- return v.Scale(1 / l)
33-}
34-func (v Vec3) Neg() Vec3 { return Vec3{-v.X, -v.Y, -v.Z} }
35-
36-// Ray is a point and a (usually normalized) direction.
37-type Ray struct {
38- Origin, Dir Vec3
39-}
40-
41-// Hit describes where a ray met a surface.
42-type Hit struct {
43- Point Vec3
44- Dist float64
45- Normal Vec3
46-}
47-
48-// Flip turns the normal toward the ray origin if needed, so lighting
49-// always sees the face the ray struck.
50-func (h *Hit) Flip(r Ray) {
51- if h.Normal.Dot(r.Dir) > 0 {
52- h.Normal = h.Normal.Neg()
53- }
54-}
deleted file mode 100644
@@ -1,54 +0,0 @@
1-package povray
2-
3-import "math"
4-
5-// Vec3 is a 3D vector.
6-type Vec3 struct {
7- X, Y, Z float64
8-}
9-
10-func (v Vec3) Add(o Vec3) Vec3 { return Vec3{v.X + o.X, v.Y + o.Y, v.Z + o.Z} }
11-func (v Vec3) Sub(o Vec3) Vec3 { return Vec3{v.X - o.X, v.Y - o.Y, v.Z - o.Z} }
12-func (v Vec3) Scale(s float64) Vec3 {
13- return Vec3{v.X * s, v.Y * s, v.Z * s}
14-}
15-func (v Vec3) Mul(o Vec3) Vec3 { return Vec3{v.X * o.X, v.Y * o.Y, v.Z * o.Z} }
16-
17-func (v Vec3) Dot(o Vec3) float64 { return v.X*o.X + v.Y*o.Y + v.Z*o.Z }
18-func (v Vec3) Cross(o Vec3) Vec3 {
19- return Vec3{
20- v.Y*o.Z - v.Z*o.Y,
21- v.Z*o.X - v.X*o.Z,
22- v.X*o.Y - v.Y*o.X,
23- }
24-}
25-
26-func (v Vec3) Len() float64 { return math.Sqrt(v.Dot(v)) }
27-func (v Vec3) Norm() Vec3 {
28- l := v.Len()
29- if l == 0 {
30- return Vec3{}
31- }
32- return v.Scale(1 / l)
33-}
34-func (v Vec3) Neg() Vec3 { return Vec3{-v.X, -v.Y, -v.Z} }
35-
36-// Ray is a point and a (usually normalized) direction.
37-type Ray struct {
38- Origin, Dir Vec3
39-}
40-
41-// Hit describes where a ray met a surface.
42-type Hit struct {
43- Point Vec3
44- Dist float64
45- Normal Vec3
46-}
47-
48-// Flip turns the normal toward the ray origin if needed, so lighting
49-// always sees the face the ray struck.
50-func (h *Hit) Flip(r Ray) {
51- if h.Normal.Dot(r.Dir) > 0 {
52- h.Normal = h.Normal.Neg()
53- }
54-}