🛟 Updated.
a334114 parent: 4cf6a27 added
demos/qwen-style/workspace/HANDOFF.md +118 -0 | new file mode 100644 | ||
| @@ -0,0 +1,118 @@ | ||
| 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). | |
| new file mode 100644 | |||
| @@ -0,0 +1,118 @@ | |||
| 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). | ||
added
demos/qwen-style/workspace/README.md +127 -0 | new file mode 100644 | ||
| @@ -0,0 +1,127 @@ | ||
| 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. | |
| new file mode 100644 | |||
| @@ -0,0 +1,127 @@ | |||
| 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. | ||
added
demos/qwen-style/workspace/ROADMAP.md +62 -0 | new file mode 100644 | ||
| @@ -0,0 +1,62 @@ | ||
| 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. | |
| new file mode 100644 | |||
| @@ -0,0 +1,62 @@ | |||
| 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. | ||
added
demos/qwen-style/workspace/camera.go +47 -0 | new file mode 100644 | ||
| @@ -0,0 +1,47 @@ | ||
| 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 } | |
| new file mode 100644 | |||
| @@ -0,0 +1,47 @@ | |||
| 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 } | ||
added
demos/qwen-style/workspace/docs/TUTORIEL.md +302 -0 | new file mode 100644 | ||
| @@ -0,0 +1,302 @@ | ||
| 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…). | |
| new file mode 100644 | |||
| @@ -0,0 +1,302 @@ | |||
| 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…). | ||
added
demos/qwen-style/workspace/examples/mirror/main.go +86 -0 | new file mode 100644 | ||
| @@ -0,0 +1,86 @@ | ||
| 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 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,86 @@ | |||
| 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 | +} | ||
added
demos/qwen-style/workspace/examples/pathtrace/main.go +67 -0 | new file mode 100644 | ||
| @@ -0,0 +1,67 @@ | ||
| 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 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,67 @@ | |||
| 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 | +} | ||
added
demos/qwen-style/workspace/examples/procedural/main.go +115 -0 | new file mode 100644 | ||
| @@ -0,0 +1,115 @@ | ||
| 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 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,115 @@ | |||
| 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 | +} | ||
added
demos/qwen-style/workspace/examples/spheres/main.go +73 -0 | new file mode 100644 | ||
| @@ -0,0 +1,73 @@ | ||
| 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 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,73 @@ | |||
| 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 | +} | ||
added
demos/qwen-style/workspace/examples/transform/main.go +77 -0 | new file mode 100644 | ||
| @@ -0,0 +1,77 @@ | ||
| 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 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,77 @@ | |||
| 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 | +} | ||
added
demos/qwen-style/workspace/go.mod +3 -0 | new file mode 100644 | ||
| @@ -0,0 +1,3 @@ | ||
| 1 | +module github.com/user/povray | |
| 2 | + | |
| 3 | +go 1.27.3 | |
| new file mode 100644 | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | +module github.com/user/povray | ||
| 2 | + | ||
| 3 | +go 1.27.3 | ||
added
demos/qwen-style/workspace/material.go +75 -0 | new file mode 100644 | ||
| @@ -0,0 +1,75 @@ | ||
| 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 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,75 @@ | |||
| 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 | +} | ||
added
demos/qwen-style/workspace/objects.go +129 -0 | new file mode 100644 | ||
| @@ -0,0 +1,129 @@ | ||
| 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 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,129 @@ | |||
| 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 | +} | ||
added
demos/qwen-style/workspace/pathtracer.go +148 -0 | new file mode 100644 | ||
| @@ -0,0 +1,148 @@ | ||
| 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 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,148 @@ | |||
| 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 | +} | ||
added
demos/qwen-style/workspace/png.go +62 -0 | new file mode 100644 | ||
| @@ -0,0 +1,62 @@ | ||
| 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 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,62 @@ | |||
| 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 | +} | ||
added
demos/qwen-style/workspace/povray_test.go +336 -0 | new file mode 100644 | ||
| @@ -0,0 +1,336 @@ | ||
| 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 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,336 @@ | |||
| 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 | +} | ||
added
demos/qwen-style/workspace/renderer.go +256 -0 | new file mode 100644 | ||
| @@ -0,0 +1,256 @@ | ||
| 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 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,256 @@ | |||
| 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 | +} | ||
added
demos/qwen-style/workspace/transform.go +181 -0 | new file mode 100644 | ||
| @@ -0,0 +1,181 @@ | ||
| 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 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,181 @@ | |||
| 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 | +} | ||
added
demos/qwen-style/workspace/vector.go +54 -0 | new file mode 100644 | ||
| @@ -0,0 +1,54 @@ | ||
| 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 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,54 @@ | |||
| 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 | +} | ||
added
demos/smollm3-demo/.gitignore +1 -0 | new file mode 100644 | ||
| @@ -0,0 +1 @@ | ||
| 1 | +!.mm | |
| new file mode 100644 | |||
| @@ -0,0 +1 @@ | |||
| 1 | +!.mm | ||
added
demos/smollm3-demo/.mm/agent.yaml +124 -0 | new file mode 100644 | ||
| @@ -0,0 +1,124 @@ | ||
| 1 | +# I'm Smollm3 — a coding agent: reads and changes the project, runs commands. | |
| 2 | +# | |
| 3 | +# Written by `mm init`. Every value says why it has that value; change | |
| 4 | +# them as you watch the agent work: | |
| 5 | +# https://rickub.com/bots-garden/mini-me/blob/main/guides/agent-settings-for-small-models.md | |
| 6 | +# | |
| 7 | +# model family: smollm3 | |
| 8 | +# usage: developer | |
| 9 | +# machine: small — a laptop, models up to about 8B | |
| 10 | +# model facts: read from llmman: native window 65536 tokens; reasoning switch enable_thinking | |
| 11 | +# | |
| 12 | +# Where the numbers come from (the tag before their comment): | |
| 13 | +# [card] from the model card or the GGUF, not measured — re-read the new card | |
| 14 | +# [guide] the settings guide's row for this machine | |
| 15 | +# [rule] a rule of thumb, not specific to this model | |
| 16 | +# | |
| 17 | +# mm or mm tui started from the project root (it finds .mm/agent.yaml) | |
| 18 | + | |
| 19 | +provider: llmman | |
| 20 | +# The model. Chosen for it (family smollm3): sampling (temperature, top_p, | |
| 21 | +# max_tokens) and sampling.chat_template_kwargs (enable_thinking). Change the | |
| 22 | +# model and redo them together — running mm init again is the quick way. | |
| 23 | +model: "hf.co/unsloth/SmolLM3-3B-GGUF:Q4_K_M" | |
| 24 | +# No baseUrl: llmman's defaults apply. For the record: | |
| 25 | +# baseUrl: "http://127.0.0.1:17434/v1" | |
| 26 | +# fallback: "http://host.docker.internal:17434/v1" # tried when baseUrl does not answer | |
| 27 | + | |
| 28 | +# 0 = mm asks the server for the window it serves (the model's native window | |
| 29 | +# is 65536 tokens; the server may serve less) | |
| 30 | +contextWindow: 0 | |
| 31 | +# your answer: every command and file change waits for your answer | |
| 32 | +yolo: false | |
| 33 | +# usage developer | |
| 34 | +bashTool: true | |
| 35 | +editTools: true | |
| 36 | +planTool: true | |
| 37 | +askUserTool: true | |
| 38 | +# family smollm3: its chat template reads the tools from xml_tools and writes | |
| 39 | +# its calls as <tool_call> text — mm sends them there and reads the calls | |
| 40 | +# back | |
| 41 | +toolFormat: xml_tools | |
| 42 | +# no skills: add a .mm/skills/ folder and set skillsDir: .mm/skills to use | |
| 43 | +# some (see | |
| 44 | +# https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/add-a-skill.md) | |
| 45 | +skillsDir: no-skills | |
| 46 | +# usage developer | |
| 47 | +maxTurns: 40 | |
| 48 | +# [guide] settings guide, row "Small model (≤ 8B), short tasks" | |
| 49 | +maxOutput: 8000 | |
| 50 | +firstTokenTimeout: 3m | |
| 51 | +# [guide] aligned on bashTimeout, as the settings guide says when bash is on | |
| 52 | +# (section 3) | |
| 53 | +watchdogTimeout: 30s | |
| 54 | +# [guide] settings guide, row "Small model (≤ 8B), short tasks" | |
| 55 | +bashTimeout: 30s | |
| 56 | + | |
| 57 | +sampling: | |
| 58 | + # [rule] precise temperature for a developer agent without reasoning (family | |
| 59 | + # smollm3) | |
| 60 | + temperature: 0.2 | |
| 61 | + # [card] family smollm3: SmolLM3-3B model card ("We recommend setting | |
| 62 | + # temperature=0.6 and top_p=0.95 in the sampling parameters"; | |
| 63 | + # generation_config.json: temperature 0.6, top_p 0.95) — one | |
| 64 | + # recommendation, used for both modes; reasoning on by default in the | |
| 65 | + # template (enable_thinking defaults to true; a /think or /no_think flag in | |
| 66 | + # the system prompt overrides the kwarg); precise: rule | |
| 67 | + top_p: 0.95 | |
| 68 | + # [rule] room for an answer or a whole file in one tool call | |
| 69 | + max_tokens: 4096 | |
| 70 | + # one tool call per reply: small models run a check before the change it | |
| 71 | + # checks has landed otherwise | |
| 72 | + parallel_tool_calls: false | |
| 73 | + chat_template_kwargs: | |
| 74 | + # reasoning off: family smollm3 is kept off — see the note at the end of | |
| 75 | + # the file (read from the chat template) | |
| 76 | + enable_thinking: false | |
| 77 | + | |
| 78 | +context: | |
| 79 | + # compress the history before the window is full (settings guide, every row) | |
| 80 | + enabled: true | |
| 81 | + # mm's default: 75 % of the window the server serves, a quarter left for the | |
| 82 | + # next question and its tool outputs | |
| 83 | + threshold: 75 | |
| 84 | + | |
| 85 | +sessions: | |
| 86 | + # your answer: every conversation is saved; /sessions lists them, /resume | |
| 87 | + # reopens one | |
| 88 | + enabled: true | |
| 89 | + # relative to the directory mm is started from: one folder per project | |
| 90 | + dir: .mm/sessions | |
| 91 | + | |
| 92 | +trace: | |
| 93 | + enabled: true | |
| 94 | + dir: .mm/traces | |
| 95 | + | |
| 96 | +agentsMd: | |
| 97 | + # An AGENTS.md in the project directory is added to the system prompt. | |
| 98 | + enabled: true | |
| 99 | + | |
| 100 | +acp: | |
| 101 | + name: i'm smollm3 | |
| 102 | + title: "I'm Smollm3 (developer)" | |
| 103 | + | |
| 104 | +system: | | |
| 105 | + Your name is I'm Smollm3. | |
| 106 | + You are a coding agent working in a terminal, in the user's project. | |
| 107 | + You have a bash tool, file tools (read_file, write_file, edit_file) and a | |
| 108 | + plan tool. | |
| 109 | + Read before you change: look at the files and run the commands that tell | |
| 110 | + you how the project works. Make the smallest change that does the job, | |
| 111 | + then run the tests or the build to check it. | |
| 112 | + Never state the output of a command you did not run in this answer. | |
| 113 | + When a task has several steps, make a plan and update it as you go. | |
| 114 | + | |
| 115 | +# This model is kept with reasoning off (enable_thinking: false), whatever | |
| 116 | +# the usage: measured 2026-11-13 on SmolLM3-3B Q4_K_M with toolFormat: | |
| 117 | +# xml_tools, with reasoning on it repeats its call after the tool's answer | |
| 118 | +# (3 runs out of 3), with it off it answers (6 out of 6). Tasks of several | |
| 119 | +# steps stay fragile at 3B. | |
| 120 | + | |
| 121 | +# Next: skills https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/add-a-skill.md | |
| 122 | +# MCP servers https://rickub.com/bots-garden/mini-me/blob/main/docs/en/tutorials/mcp-getting-started.md | |
| 123 | +# hooks https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/guard-the-agent-with-hooks.md | |
| 124 | +# sessions https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/keep-sessions.md | |
| new file mode 100644 | |||
| @@ -0,0 +1,124 @@ | |||
| 1 | +# I'm Smollm3 — a coding agent: reads and changes the project, runs commands. | ||
| 2 | +# | ||
| 3 | +# Written by `mm init`. Every value says why it has that value; change | ||
| 4 | +# them as you watch the agent work: | ||
| 5 | +# https://rickub.com/bots-garden/mini-me/blob/main/guides/agent-settings-for-small-models.md | ||
| 6 | +# | ||
| 7 | +# model family: smollm3 | ||
| 8 | +# usage: developer | ||
| 9 | +# machine: small — a laptop, models up to about 8B | ||
| 10 | +# model facts: read from llmman: native window 65536 tokens; reasoning switch enable_thinking | ||
| 11 | +# | ||
| 12 | +# Where the numbers come from (the tag before their comment): | ||
| 13 | +# [card] from the model card or the GGUF, not measured — re-read the new card | ||
| 14 | +# [guide] the settings guide's row for this machine | ||
| 15 | +# [rule] a rule of thumb, not specific to this model | ||
| 16 | +# | ||
| 17 | +# mm or mm tui started from the project root (it finds .mm/agent.yaml) | ||
| 18 | + | ||
| 19 | +provider: llmman | ||
| 20 | +# The model. Chosen for it (family smollm3): sampling (temperature, top_p, | ||
| 21 | +# max_tokens) and sampling.chat_template_kwargs (enable_thinking). Change the | ||
| 22 | +# model and redo them together — running mm init again is the quick way. | ||
| 23 | +model: "hf.co/unsloth/SmolLM3-3B-GGUF:Q4_K_M" | ||
| 24 | +# No baseUrl: llmman's defaults apply. For the record: | ||
| 25 | +# baseUrl: "http://127.0.0.1:17434/v1" | ||
| 26 | +# fallback: "http://host.docker.internal:17434/v1" # tried when baseUrl does not answer | ||
| 27 | + | ||
| 28 | +# 0 = mm asks the server for the window it serves (the model's native window | ||
| 29 | +# is 65536 tokens; the server may serve less) | ||
| 30 | +contextWindow: 0 | ||
| 31 | +# your answer: every command and file change waits for your answer | ||
| 32 | +yolo: false | ||
| 33 | +# usage developer | ||
| 34 | +bashTool: true | ||
| 35 | +editTools: true | ||
| 36 | +planTool: true | ||
| 37 | +askUserTool: true | ||
| 38 | +# family smollm3: its chat template reads the tools from xml_tools and writes | ||
| 39 | +# its calls as <tool_call> text — mm sends them there and reads the calls | ||
| 40 | +# back | ||
| 41 | +toolFormat: xml_tools | ||
| 42 | +# no skills: add a .mm/skills/ folder and set skillsDir: .mm/skills to use | ||
| 43 | +# some (see | ||
| 44 | +# https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/add-a-skill.md) | ||
| 45 | +skillsDir: no-skills | ||
| 46 | +# usage developer | ||
| 47 | +maxTurns: 40 | ||
| 48 | +# [guide] settings guide, row "Small model (≤ 8B), short tasks" | ||
| 49 | +maxOutput: 8000 | ||
| 50 | +firstTokenTimeout: 3m | ||
| 51 | +# [guide] aligned on bashTimeout, as the settings guide says when bash is on | ||
| 52 | +# (section 3) | ||
| 53 | +watchdogTimeout: 30s | ||
| 54 | +# [guide] settings guide, row "Small model (≤ 8B), short tasks" | ||
| 55 | +bashTimeout: 30s | ||
| 56 | + | ||
| 57 | +sampling: | ||
| 58 | + # [rule] precise temperature for a developer agent without reasoning (family | ||
| 59 | + # smollm3) | ||
| 60 | + temperature: 0.2 | ||
| 61 | + # [card] family smollm3: SmolLM3-3B model card ("We recommend setting | ||
| 62 | + # temperature=0.6 and top_p=0.95 in the sampling parameters"; | ||
| 63 | + # generation_config.json: temperature 0.6, top_p 0.95) — one | ||
| 64 | + # recommendation, used for both modes; reasoning on by default in the | ||
| 65 | + # template (enable_thinking defaults to true; a /think or /no_think flag in | ||
| 66 | + # the system prompt overrides the kwarg); precise: rule | ||
| 67 | + top_p: 0.95 | ||
| 68 | + # [rule] room for an answer or a whole file in one tool call | ||
| 69 | + max_tokens: 4096 | ||
| 70 | + # one tool call per reply: small models run a check before the change it | ||
| 71 | + # checks has landed otherwise | ||
| 72 | + parallel_tool_calls: false | ||
| 73 | + chat_template_kwargs: | ||
| 74 | + # reasoning off: family smollm3 is kept off — see the note at the end of | ||
| 75 | + # the file (read from the chat template) | ||
| 76 | + enable_thinking: false | ||
| 77 | + | ||
| 78 | +context: | ||
| 79 | + # compress the history before the window is full (settings guide, every row) | ||
| 80 | + enabled: true | ||
| 81 | + # mm's default: 75 % of the window the server serves, a quarter left for the | ||
| 82 | + # next question and its tool outputs | ||
| 83 | + threshold: 75 | ||
| 84 | + | ||
| 85 | +sessions: | ||
| 86 | + # your answer: every conversation is saved; /sessions lists them, /resume | ||
| 87 | + # reopens one | ||
| 88 | + enabled: true | ||
| 89 | + # relative to the directory mm is started from: one folder per project | ||
| 90 | + dir: .mm/sessions | ||
| 91 | + | ||
| 92 | +trace: | ||
| 93 | + enabled: true | ||
| 94 | + dir: .mm/traces | ||
| 95 | + | ||
| 96 | +agentsMd: | ||
| 97 | + # An AGENTS.md in the project directory is added to the system prompt. | ||
| 98 | + enabled: true | ||
| 99 | + | ||
| 100 | +acp: | ||
| 101 | + name: i'm smollm3 | ||
| 102 | + title: "I'm Smollm3 (developer)" | ||
| 103 | + | ||
| 104 | +system: | | ||
| 105 | + Your name is I'm Smollm3. | ||
| 106 | + You are a coding agent working in a terminal, in the user's project. | ||
| 107 | + You have a bash tool, file tools (read_file, write_file, edit_file) and a | ||
| 108 | + plan tool. | ||
| 109 | + Read before you change: look at the files and run the commands that tell | ||
| 110 | + you how the project works. Make the smallest change that does the job, | ||
| 111 | + then run the tests or the build to check it. | ||
| 112 | + Never state the output of a command you did not run in this answer. | ||
| 113 | + When a task has several steps, make a plan and update it as you go. | ||
| 114 | + | ||
| 115 | +# This model is kept with reasoning off (enable_thinking: false), whatever | ||
| 116 | +# the usage: measured 2026-11-13 on SmolLM3-3B Q4_K_M with toolFormat: | ||
| 117 | +# xml_tools, with reasoning on it repeats its call after the tool's answer | ||
| 118 | +# (3 runs out of 3), with it off it answers (6 out of 6). Tasks of several | ||
| 119 | +# steps stay fragile at 3B. | ||
| 120 | + | ||
| 121 | +# Next: skills https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/add-a-skill.md | ||
| 122 | +# MCP servers https://rickub.com/bots-garden/mini-me/blob/main/docs/en/tutorials/mcp-getting-started.md | ||
| 123 | +# hooks https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/guard-the-agent-with-hooks.md | ||
| 124 | +# sessions https://rickub.com/bots-garden/mini-me/blob/main/docs/en/how-to/keep-sessions.md | ||