Repository: flannelhead/blackstar
Branch: master
Commit: 40f1f1965292
Files: 26
Total size: 43.3 KB
Directory structure:
gitextract_5lpryipi/
├── .gitignore
├── LICENSE
├── README.md
├── Setup.hs
├── animations/
│ └── default-ani.yaml
├── app/
│ ├── Animate.hs
│ ├── GenerateTree.hs
│ └── Main.hs
├── blackstar.cabal
├── scenes/
│ ├── closeup.yaml
│ ├── default-aa.yaml
│ ├── default.yaml
│ ├── fartheraway.yaml
│ ├── lensing-disk.yaml
│ ├── lensing.yaml
│ ├── wideangle-disk.yaml
│ ├── wideangle.yaml
│ └── wideangle1.yaml
├── scripts/
│ └── ffmpeg-animate
├── src/
│ ├── Animation.hs
│ ├── ConfigFile.hs
│ ├── ImageFilters.hs
│ ├── Raytracer.hs
│ ├── StarMap.hs
│ └── Util.hs
└── stack.yaml
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.stack-work
blackstar.prof
out.png
texture.jpg
BSC5
SAO.pc
PPM
PPMra
renders
bloomed.png
output
*.png
!example.png
frames*
*.mkv
dist*
.ghc.*
================================================
FILE: LICENSE
================================================
Copyright Sakari Kapanen (c) 2016
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Author name here nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: README.md
================================================
# Blackstar
A black hole ray tracer written in Haskell. There's [an article](https://flannelhead.github.io/projects/blackstar.html) about this on my homepage. I've also written a [theoretical writeup](https://flannelhead.github.io/posts/2016-03-06-photons-and-black-holes.html) on Schwarzschild geodesics.

## Features
* Fast, parallel ray tracing
* Rendering [Schwarzschild](https://en.wikipedia.org/wiki/Schwarzschild_metric) black holes
* Rendering accretion disks
* Drawing the celestial sphere using a star catalogue
* Bloom effect
* Antialiasing by 4x supersampling for smoother images
* Easy, YAML based configuration
* A simple CLI
* Batch mode and sequence generator for creating animations
## What about the name?
It is a tribute to David Bowie, referring to his last album.
## Building
Use [`stack`](http://docs.haskellstack.org/en/stable/README/) to build this. First clone the repo, then run `stack build` and follow the instructions given by `stack`. You should be able to build `blackstar` on any platform where you can install `stack`.
You will have to build the star lookup tree first. Download the [PPM star catalog](http://tdc-www.harvard.edu/software/catalogs/ppm.html) [this archive](http://tdc-www.harvard.edu/software/catalogs/ppm.tar.gz) and extract the file `PPM` to the root folder of this project. Then run `stack exec generate-tree PPM stars.kdt` and the tree will be generated and saved to the file `stars.kdt`.
### Speeding it up with LLVM
When doing large or batch renders, it is recommended to build `blackstar` using GHC's LLVM backend. GHC produces LLVM bytecode and LLVM produces fast native code from GHC's output. In my tests I've noticed ~1.5x speedups.
The LLVM backend isn't used by default since one needs to install (and usually build) a specific version of LLVM separately. Moreover, the build time is significantly higher with LLVM, so one doesn't definitely want to use it while hacking on the code.
To successfully build with LLVM, you need to:
* Download and [build](http://llvm.org/docs/GettingStarted.html#getting-started-quickly-a-summary) [LLVM 6.0.1](http://llvm.org/releases/download.html#6.0.1). You can skip the Clang parts. After the build, you should make sure the tools `llc` and `opt` are found in your `PATH`. Notice that these aren't included in the prebuilt LLVM binaries, that's why you'll need to build it.
* Build `blackstar` with `stack build --ghc-options -fllvm`. (If you've just built it, run `stack clean` first to ensure it really gets rebuilt with LLVM.)
* Wait patiently
* Enjoy the result!
You don't necessarily have to use LLVM at all. However, if you can acquire binaries of the right LLVM version, that will give you some speedups.
## Usage
When `blackstar` has been built with `stack`, you can run it with
```
stack exec blackstar -- [-p|--preview] [-f|--force] [-o|--output=PATH] [-s|--starmap=PATH] SCENENAME
```
Notice the two dashes (`--`) which are required to terminate `stack`'s argument list.
`cabal` users can run `blackstar` by executing
```
cabal run -- [OPTIONS] SCENENAME
```
in the root folder of the project.
Scenes are defined using YAML config files. Look in the `scenes` folder for examples. To render the `default` scene to the directory `output`, run
```
stack exec blackstar -- scenes/default.yaml --output output
```
in the root directory of the project. The `--output` flag specifies the output directory. By default, `blackstar` searches for a starmap in the path `./stars.kdt`, but a different path can be specified using the `--starmap` flag.
The rendered files are named `scenename.png` and `scenename-bloomed.png`. The `--preview` flag can be used to render small-sized previews of the scene while adjusting the parameters. The `--force` flag will cause `blackstar` to overwrite output images without a prompt.
If a directory is given as the input scene path, `blackstar` searches non-recursively for YAML files in that directory and tries to render them. The scenes are placed in the specified output directory.
There's also a help text which can be seen by running
```
stack exec blackstar -- --help
OR
cabal run -- --help
```
Better images can be achieved by rendering larger than the target size and then scaling down (some antialiasing is achieved). This is called supersampling and is implemented in `blackstar`. It can be enabled by setting `supersampling` to true in the YAML config file — see `scenes/default-aa.yaml` for an example.
## Animation
There is a separate YAML config format for specifying animations. For example, see [default-ani.yaml](animations/default-ani.yaml).
In the first pass, the animation file must be rendered into separate config files for each frame. The `animate` executable takes care of this. First, create a directory where the frame config files will be put.
```
mkdir frames
```
Then run `animate`:
```
stack exec animate -- animations/default-ani.yaml -o frames
```
Now you should find quite a bunch of `.yaml` files in the folder `frames`.
Make another folder for the output frames:
```
mkdir frames-out
```
Now you will be able to run `blackstar` in batch mode to render the frames:
```
stack exec blackstar -- frames -o frames-out
```
This will take quite a while.
After the frames have been rendered, generate a video from the `*.png` still with your utility of preference. You can also use my script `scripts/ffmpeg-animate`, which uses `ffmpeg`. You only need to give it the prefix of the numbered frames:
```
scripts/ffmpeg-animate frames-out/default-ani
```
The output video will be rendered to `out.mkv`.
## Profiling
Thanks to `stack`, profiling is incredibly easy. Rebuild `blackstar` by running
```
stack build --profile
```
and then run it with
```
stack exec blackstar -- scenes/default.yaml -o output +RTS -p
```
The profile will be generated to `blackstar.prof`.
## TODO
As always, there's a plenty of room for improvement. For example:
* Animation: mathematically rigorous non-stationary observer
* Arbitrary textures for accretion disk (or some cool noise generator)
* Redshifting of the accretion disk
* Preview / scene planner GUI ([fltkhs](https://hackage.haskell.org/package/fltkhs))
Pull requests are welcome! If you find some cool scenes, I'd appreciate if you contributed them to this repository.
================================================
FILE: Setup.hs
================================================
import Distribution.Simple
main = defaultMain
================================================
FILE: animations/default-ani.yaml
================================================
# Animation files are quite similar to scene files. The difference is that you
# can define multiple camera instances which are then fixed to certain points
# of time (normalized to [0, 1[). The frames in between are then generated by
# interpolating between the keyframes using the interpolation method of choice.
# Scene config is similar to the scene files
scene:
resolution: [1920, 1080]
bloomStrength: 0.7
starIntensity: 0.7
starSaturation: 0.7
diskHSV: [180, 0.1, 1.05]
diskOpacity: 0.95
diskInner: 1.8
diskOuter: 13
supersampling: true
# The number of frames in the animation
nFrames: 375
# Interpolation method. Currently, 'linear' is the only option
interpolation: 'linear'
# Specify any number of keyframes you want
keyframes:
- time: 0 # The normalize time of the keyframe. 0 is the first frame, 1 is the last
camera: # Camera config is similar to the scene files
position: [3, 3, -20]
lookAt: [-7, 5, 0]
upVec: [-0.2, 1, 0]
fov: 1.5
- time: 1
camera:
position: [-15, 1, -20]
lookAt: [13, -7, 0]
upVec: [-0.2, 1, 0]
fov: 2
================================================
FILE: app/Animate.hs
================================================
{-# LANGUAGE DeriveDataTypeable #-}
{-# OPTIONS_GHC -fno-cse #-}
module Main where
import Prelude hiding (writeFile)
import System.Directory
import System.FilePath
import Data.Yaml (decodeFileEither, prettyPrintParseException, encode)
import System.Console.CmdArgs
import Control.Monad (forM_)
import Data.ByteString.Lazy (fromStrict, writeFile)
import Animation
import Util
data Animate = Animate { inFile :: FilePath
, output :: FilePath
, force :: Bool }
deriving (Show, Data, Typeable)
argparser :: Animate
argparser = Animate { inFile = def
&= argPos 0
&= typ "INPUTFILE"
, output = ""
&= help "output directory"
&= typ "PATH"
, force = def
&= help "overwrite images without asking" }
&= summary "Animation helper for Blackstar"
main :: IO ()
main = do
cmdline <- cmdArgs argparser
let inPath = inFile cmdline
let basename = takeBaseName inPath
inputExists <- doesFileExist inPath
outPath <- normalizePath =<< case output cmdline of
"" -> getCurrentDirectory
x -> return x
createDirectoryIfMissing True outPath
if inputExists then do
config <- decodeFileEither inPath
case config of
Right cfg ->
case validateKeyframes $ keyframes cfg of
Right () -> do
let nFr = nFrames cfg
forM_ (zip (generateFrames cfg) [(0 :: Int), 1 ..])
(\(frame, idx) -> do
let filename = outPath </> basename ++ "_" ++
padZero (nFr - 1) idx <.> ".yaml"
let outBl = fromStrict $ encode frame
if force cmdline
then writeFile filename outBl
else promptOverwriteFile filename
(\fname -> writeFile fname outBl)
)
Left err -> putStrLn err
Left err -> putStrLn $ "Error when decoding config:\n" ++
prettyPrintParseException err
else putStrLn "Couldn't open input file."
================================================
FILE: app/GenerateTree.hs
================================================
module Main where
import qualified Data.ByteString.Lazy as B
import System.Environment (getArgs)
import Util
import StarMap
-- Generate and store the k-d star tree from a star catalog
main :: IO ()
main = do
args <- getArgs
case args of
[infile, outfile] -> do
outfile' <- normalizePath outfile
infile' <- normalizePath infile
eitherMap <- readMapFromFile infile'
case eitherMap of
Right stars -> do
putStrLn "Generating the star tree..."
tree <- timeAction "Building the tree"
$ buildStarTree stars
let treeBl = B.fromStrict $ treeToByteString tree
promptOverwriteFile outfile'
(\filename -> B.writeFile filename treeBl)
putStrLn $ "Tree saved to " ++ outfile' ++ "."
Left err -> putStrLn err
_ -> putStrLn "Usage: generate-tree <INFILE> <OUTFILE>"
================================================
FILE: app/Main.hs
================================================
{-# LANGUAGE DeriveDataTypeable #-}
{-# OPTIONS_GHC -fno-cse #-}
module Main where
import System.Directory
import Control.Monad (when, forM_)
import Data.Yaml (decodeFileEither, prettyPrintParseException)
import System.Console.CmdArgs
import System.FilePath (takeBaseName, takeExtension, (</>), (<.>))
import Data.List (sort)
import System.Console.ANSI (clearScreen, setCursorPosition)
import Raytracer
import StarMap
import ConfigFile
import ImageFilters
import Util
data Blackstar = Blackstar { preview :: Bool
, output :: String
, force :: Bool
, starmap :: String
, inputfile :: String }
deriving (Show, Data, Typeable)
argparser :: Blackstar
argparser = Blackstar { preview = def
&= help "preview render (small size)"
, output = ""
&= help "output directory"
&= typ "PATH"
, force = def
&= help "overwrite images without asking"
, starmap = "stars.kdt"
&= help "path to starmap"
&= typ "PATH"
, inputfile = def
&= argPos 0
&= typ "INPUTFILE"
} &= summary "Blackstar v0.1"
main :: IO ()
main = do
cmdline <- cmdArgs argparser
etree <- readTreeFromFile $ starmap cmdline
case etree of
Right tree -> putStrLn "Starmap successfully read."
>> doStart cmdline tree
Left err -> putStrLn $ "Error decoding star tree: \n" ++ err
doStart :: Blackstar -> StarTree -> IO ()
doStart cmdline tree = do
-- Resolve the output directory
when (output cmdline /= "")
$ createDirectoryIfMissing True (output cmdline)
outdir <- normalizePath =<< case output cmdline of
"" -> getCurrentDirectory
x -> return x
createDirectoryIfMissing True outdir
-- Resolve the input file or directory
filename <- normalizePath $ inputfile cmdline
isDir <- doesDirectoryExist filename
if isDir then do
putStrLn $ filename
++ " is a directory. Rendering all scenes inside it..."
inputFiles <- map (filename </>)
. sort . filter (\f -> takeExtension f == ".yaml")
<$> getDirectoryContents filename
forM_ (zip inputFiles [(1 :: Int)..]) $ \(scn, idx) -> do
clearScreen
setCursorPosition 0 0
putStrLn $ "Batch mode progress: " ++ show idx ++ "/"
++ show (length inputFiles)
handleScene cmdline tree outdir scn
else handleScene cmdline tree outdir filename
handleScene :: Blackstar -> StarTree -> String -> String -> IO ()
handleScene cmdline tree outdir filename = do
let pvw = preview cmdline
let sceneName = takeBaseName filename
putStrLn $ "Reading " ++ filename ++ "..."
cfg <- decodeFileEither filename
let sceneName' = if pvw then "prev-" ++ sceneName else sceneName
case cfg of
Right config -> putStrLn "Scene successfully read."
>> doRender cmdline (prepareScene config pvw) tree
sceneName' outdir
Left err -> putStrLn $ prettyPrintParseException err
prepareScene :: Config -> Bool -> Config
prepareScene cfg doPreview = let
scn = scene cfg
(w, h) = resolution scn
res = 300
newRes = if w >= h then (res, res * h `div` w) else (res * w `div` h, res)
newScn = if doPreview then scn { resolution = newRes
, supersampling = False
, bloomStrength = 0 }
else scn
in cfg { scene = newScn }
doRender :: Blackstar -> Config -> StarTree -> String -> String -> IO ()
doRender cmdline cfg tree sceneName outdir = do
putStrLn $ "Rendering " ++ sceneName ++ "..."
let scn = scene cfg
img <- timeAction "Rendering" $ render cfg tree
let outName = outdir </> sceneName <.> ".png"
final <- if bloomStrength scn /= 0
then do
putStrLn "Applying bloom..."
bloomed <- bloom (bloomStrength scn) (bloomDivider scn) img
timeAction "Bloom" bloomed
else return img
putStrLn $ "Saving to " ++ outName ++ "..."
if force cmdline
then writeImg final outName
else promptOverwriteFile outName (writeImg final)
putStrLn "Everything done. Thank you!"
================================================
FILE: blackstar.cabal
================================================
name: blackstar
version: 0.1.0.0
synopsis: A black hole ray tracer
description: A black hole ray tracer written in Haskell
homepage: http://github.com/flannelhead/blackstar#readme
license: BSD3
license-file: LICENSE
author: Sakari Kapanen
maintainer: sakari.m.kapanen@gmail.com
copyright: 2019 Sakari Kapanen
category: Graphics
build-type: Simple
-- extra-source-files:
cabal-version: >=1.10
library
hs-source-dirs: src
exposed-modules: Raytracer
, StarMap
, ConfigFile
, ImageFilters
, Util
, Animation
ghc-options: -Wall
-O2
build-depends: base >= 4.7 && < 5
, data-default
, linear
, lens
, bytestring
, cereal
, vector
, kdt
, yaml
, aeson
, directory
, massiv
, massiv-io
, filepath
, time
, deepseq
default-language: Haskell2010
executable blackstar
hs-source-dirs: app
main-is: Main.hs
ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
build-depends: base
, blackstar
, directory
, yaml
, bytestring
, cmdargs
, filepath
, ansi-terminal
default-language: Haskell2010
executable animate
hs-source-dirs: app
main-is: Animate.hs
ghc-options: -Wall
build-depends: base
, blackstar
, directory
, yaml
, bytestring
, cmdargs
, filepath
default-language: Haskell2010
executable generate-tree
hs-source-dirs: app
main-is: GenerateTree.hs
ghc-options: -Wall
build-depends: base
, blackstar
, directory
, bytestring
default-language: Haskell2010
source-repository head
type: git
location: https://github.com/flannelhead/blackstar
================================================
FILE: scenes/closeup.yaml
================================================
camera:
position: [10, 1, -2]
lookAt: [0, 0, 6]
upVec: [0, 1, 0]
fov: 1.2
scene:
resolution: [1280, 960]
bloomStrength: 0.7
starIntensity: 0.7
starSaturation: 0.7
diskOpacity: 0.95
diskInner: 3
diskOuter: 9
================================================
FILE: scenes/default-aa.yaml
================================================
camera:
position: [0, 1, -20]
lookAt: [2, 0, 0]
upVec: [-0.2, 1, 0]
fov: 1.5
scene:
resolution: [1920, 1080]
bloomStrength: 0.15
starIntensity: 0.4
starSaturation: 1.5
diskColor: [180, 0.1, 1.05]
diskOpacity: 0.95
diskInner: 1.8
diskOuter: 13
supersampling: true
================================================
FILE: scenes/default.yaml
================================================
camera:
# All the vectors are [x, y, z] coordinates
position: [0, 1, -20] # The position of the camera
lookAt: [2, 0, 0] # The point to look at
upVec: [-0.2, 1, 0] # The "up" direction vector which determines the
# orientation of the camera
fov: 1.5 # The tangent of the view angle
scene:
resolution: [1920, 1080] # [width, height] of the image
bloomStrength: 0.15 # The strength (weight) of the bloom effect. Setting this to
# 0 disables it entirely
bloomDivider: 25 # A number x such that r = image width / x is the bloom radius
starIntensity: 0.4 # The intensity (0 = black, 1 = white) of the stars
starSaturation: 1.5 # The color saturation of the stars
diskOpacity: 0.95 # Opacity of the accretion disk (0 = fully transparent,
# 1 = fully opaque)
diskInner: 1.8 # The inner radius of the accretion disk
diskOuter: 13 # The outer radius of the accretion disk
diskColor: [180, 0.1, 1.05] # The colour of the accretion disk in the HSI color space.
# H: 0..360, S: 0..1, I: 0..1
supersampling: false # Set this to true to enable smoothing by supersampling
# a 4x sized image
stepSize: 0.3 # The size of the timestep in the simulation. Usually this value
# should be fine.
================================================
FILE: scenes/fartheraway.yaml
================================================
camera:
position: [-25, 1, -60]
lookAt: [-12, -4, 0]
upVec: [0.15, 1, 0]
fov: 2
scene:
resolution: [1920, 1080]
bloomStrength: 0.15
starIntensity: 0.4
starSaturation: 1.5
diskOpacity: 0.95
diskInner: 3
diskOuter: 12
supersampling: true
================================================
FILE: scenes/lensing-disk.yaml
================================================
camera:
position: [30, 0.4, 3]
lookAt: [0, 0, 0]
upVec: [0, 1, 0.2]
fov: 1
scene:
resolution: [1280, 800]
bloomStrength: 0.15
starIntensity: 0.4
starSaturation: 1.5
diskOpacity: 0.95
diskInner: 3
diskOuter: 12
supersampling: true
================================================
FILE: scenes/lensing.yaml
================================================
camera:
position: [30, 0.4, 3]
lookAt: [0, 0, 0]
upVec: [0, 1, 0.2]
fov: 1
scene:
resolution: [1600, 1200]
bloomStrength: 0.15
starIntensity: 0.4
starSaturation: 1.5
diskOpacity: 0
diskInner: 3
diskOuter: 12
supersampling: true
================================================
FILE: scenes/wideangle-disk.yaml
================================================
camera:
position: [-6, 1, -20]
lookAt: [-6, -4, 0]
upVec: [-0.2, 1, 0]
fov: 3.5
scene:
resolution: [1920, 1080]
bloomStrength: 0.15
starIntensity: 0.4
starSaturation: 1.5
diskOpacity: 0.95
diskInner: 2.5
diskOuter: 12
supersampling: true
================================================
FILE: scenes/wideangle.yaml
================================================
camera:
position: [20, 0, 0]
lookAt: [0, 0, 3.5]
upVec: [0, 1, 0]
fov: 2
scene:
resolution: [1920, 1020]
bloomStrength: 0.15
starIntensity: 0.4
starSaturation: 1.5
diskOpacity: 0
supersampling: true
================================================
FILE: scenes/wideangle1.yaml
================================================
camera:
position: [0, 0, 20]
lookAt: [3.5, 0, 0]
upVec: [0, 1, 0]
fov: 2
scene:
resolution: [1920, 1080]
bloomStrength: 0.15
starIntensity: 0.4
starSaturation: 1.5
diskOpacity: 0
supersampling: true
================================================
FILE: scripts/ffmpeg-animate
================================================
#!/bin/sh
# Create YouTube quality video from stills using ffmpeg
#
# USAGE:
# ffmpeg-animate PREFIX
#
# Writes to file "out.mkv" in the current directory
ffmpeg -f image2 -i "${1}_%03d.png" \
-c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p \
-r 25 out.mkv
================================================
FILE: src/Animation.hs
================================================
{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveGeneric #-}
module Animation ( Keyframe(camera, time)
, Animation(scene, nFrames, interpolation, keyframes)
, InterpolationMethod(Linear)
, generateFrames, validateKeyframes ) where
import Data.List (sortBy)
import Data.Ord (comparing)
import qualified ConfigFile as CF
import Data.Aeson.Types
import Linear ((*^))
import GHC.Generics
data Keyframe = Keyframe { camera :: CF.Camera
, time :: Double }
deriving (Generic)
data InterpolationMethod = Linear
data Animation = Animation { scene :: CF.Scene
, nFrames :: Int
, interpolation :: InterpolationMethod
, keyframes :: [Keyframe] }
deriving (Generic)
instance FromJSON Keyframe
instance FromJSON InterpolationMethod where
parseJSON str = do
(str' :: String) <- parseJSON str
return $ case str' of
"linear" -> Linear
_ -> Linear
instance FromJSON Animation
validateKeyframes :: [Keyframe] -> Either String ()
validateKeyframes [] = Left "Must have at least two keyframes"
validateKeyframes [_] = validateKeyframes []
validateKeyframes frs = if time (head frs) == 0 && time (last frs) == 1
then Right ()
else Left "First keyframe must have time == 0, last time == 1"
generateFrames :: Animation -> [CF.Config]
generateFrames animation = let
stepsize = (1 :: Double) / fromIntegral (nFrames animation - 1)
-- Take the first keyframe from the scene in the config
-- Also sort the frames by time
frames = sortBy (comparing time) $ keyframes animation
points = (* stepsize) . fromIntegral <$> [0 .. nFrames animation - 1]
in map (makeFrame animation frames) points
makeFrame :: Animation -> [Keyframe] -> Double -> CF.Config
makeFrame animation frames point = let
scn = scene animation
mtd = interpolation animation
in CF.Config { CF.camera = interpolate mtd frames point
, CF.scene = scn }
interpolate :: InterpolationMethod -> [Keyframe] -> Double -> CF.Camera
interpolate method frames t = let
findFrames (fr1 : fr2 : frs) = if t >= time fr1 && t < time fr2
then (fr1, fr2)
else findFrames (fr2 : frs)
findFrames [fr] = (fr, fr { time = time fr + 1 } )
(f1, f2) = findFrames frames
t' = (t - time f1) / (time f2 - time f1)
f :: Fractional a => (Double -> a -> a) -> a -> a -> a
f = interpolationFunction method t'
cam1 = camera f1
cam2 = camera f2
in CF.Camera { CF.fov = f (*) (CF.fov cam1) (CF.fov cam2)
, CF.position = f (*^) (CF.position cam1) (CF.position cam2)
, CF.lookAt = f (*^) (CF.lookAt cam1) (CF.lookAt cam2)
, CF.upVec = f (*^) (CF.upVec cam1) (CF.upVec cam2) }
interpolationFunction :: Fractional a => InterpolationMethod -> Double
-> (Double -> a -> a)
-> a -> a -> a
{-# INLINE interpolationFunction #-}
interpolationFunction method t times a b = case method of
Linear -> a + t `times` (b - a)
================================================
FILE: src/ConfigFile.hs
================================================
{-# LANGUAGE OverloadedStrings, FlexibleInstances, DeriveGeneric #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module ConfigFile
( Scene( Scene, safeDistance, stepSize, bloomStrength, bloomDivider
, starIntensity, starSaturation, supersampling
, diskColor, diskOpacity, diskInner, diskOuter, resolution )
, Camera( Camera, position, lookAt, upVec, fov )
, Config( Config, camera, scene ) ) where
import Data.Aeson.Types
import Linear
import GHC.Generics
import Graphics.ColorSpace
data Config = Config { scene :: Scene
, camera :: Camera }
deriving (Generic)
data Scene = Scene { safeDistance :: !Double
, stepSize :: !Double
, bloomStrength :: !Double
, bloomDivider :: !Int
, starIntensity :: !Double
, starSaturation :: !Double
, diskColor :: !(Pixel HSI Double)
, diskOpacity :: !Double
, diskInner :: !Double
, diskOuter :: !Double
, resolution :: !(Int, Int)
, supersampling :: !Bool }
deriving (Generic)
data Camera = Camera { position :: !(V3 Double)
, lookAt :: !(V3 Double)
, upVec :: !(V3 Double)
, fov :: !Double }
deriving (Generic)
instance FromJSON (V3 Double) where
parseJSON vec = do
[x, y, z] <- parseJSON vec
return $ V3 x y z
instance ToJSON (V3 Double) where
toJSON (V3 x y z) = toJSON [x, y, z]
instance FromJSON (Pixel HSI Double) where
parseJSON hsi = do
[x, y, z] <- parseJSON hsi
return $ PixelHSI (x / 360) y z
instance ToJSON (Pixel HSI Double) where
toJSON (PixelHSI h s i) = toJSON [360 * h, s, i]
instance FromJSON Config
instance ToJSON Config where
toEncoding = genericToEncoding defaultOptions
instance FromJSON Camera
instance ToJSON Camera where
toEncoding = genericToEncoding defaultOptions
instance FromJSON Scene where
parseJSON (Object v) = Scene 0 <$>
v .:? "stepSize" .!= 0.3 <*>
v .:? "bloomStrength" .!= 0.4 <*>
v .:? "bloomDivider" .!= 25 <*>
v .:? "starIntensity" .!= 0.7 <*>
v .:? "starSaturation" .!= 0.7 <*>
v .:? "diskColor"
.!= PixelHSI 0.16 0.1 0.95 <*>
v .:? "diskOpacity" .!= 0 <*>
v .:? "diskInner" .!= 3 <*>
v .:? "diskOuter" .!= 12 <*>
v .:? "resolution" .!= (1280, 720) <*>
v .:? "supersampling" .!= False
parseJSON invalid = typeMismatch "Object" invalid
instance ToJSON Scene where
toEncoding = genericToEncoding defaultOptions
================================================
FILE: src/ImageFilters.hs
================================================
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE Strict #-}
module ImageFilters (bloom, supersample) where
import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Unboxed.Mutable as MU
import Data.Massiv.Array
import Data.Massiv.Array.Manifest.Vector
import Data.Massiv.Array.Unsafe
import Data.Massiv.Array.IO
import Control.Monad (replicateM_)
import Control.Applicative (liftA2)
import Graphics.ColorSpace
ix1d :: Int -> Int -> Int -> Int
{-# INLINE ix1d #-}
ix1d !w !y !x = y*w + x
add :: Pixel RGB Double -> Pixel RGB Double -> Pixel RGB Double
add = liftA2 (+)
sub :: Pixel RGB Double -> Pixel RGB Double -> Pixel RGB Double
sub = liftA2 (-)
mul :: Double -> Pixel RGB Double -> Pixel RGB Double
mul a = fmap (a *)
boxBlur :: Int -> Int -> Image U RGB Double -> IO (Image U RGB Double)
boxBlur !r !passes img = let
myDims@(h :. w) = size img
rows' = U.enumFromN (0 :: Int) h
cols' = U.enumFromN (0 :: Int) w
-- Functions to safely index a vector representing an image with specialized
-- horizontal / vertical bound checks. Out of bounds indices return a black
-- pixel.
{-# INLINE ixh #-}
{-# INLINE ixv #-}
{-# INLINE ix1d' #-}
ix1d' = ix1d w
ixh v y x
| x < 0 || x >= w = PixelRGB 0 0 0
| otherwise = U.unsafeIndex v $ ix1d' y x
ixv v x y
| y < 0 || y >= h = PixelRGB 0 0 0
| otherwise = U.unsafeIndex v $ ix1d' y x
-- Normalize by the "width" of the kernel
normFactor :: Double
{-# INLINE normFactor #-}
normFactor = 1 / (2*fromIntegral r + 1)
{-# INLINE blur #-}
blur writeToVec crds ix1df readf vecIn y = let
{-# INLINE pix #-}
-- A function to yield a pixel from the image vector
pix = readf vecIn y
-- Starting value
startVal = U.foldl1' add . U.map pix . U.unsafeTake r $ crds
{-# INLINE accumulate #-}
accumulate !rgb x = do
let newRGB = (rgb `add` pix (x+r)) `sub` pix (x-r)
_ <- writeToVec (ix1df y x) $ mul normFactor newRGB
return newRGB
-- Sweep over the row / col of the image
in U.foldM'_ accumulate startVal crds
in do
mv <- U.thaw $ toVector img
let wrt = MU.unsafeWrite mv
replicateM_ passes $ do
-- First blur horizontally
tmp1 <- U.freeze mv
U.mapM_ (blur wrt cols' ix1d' ixh tmp1) rows'
-- Then vertically
tmp2 <- U.freeze mv
U.mapM_ (blur wrt rows' (flip ix1d') ixv tmp2) cols'
out <- U.unsafeFreeze mv
return $ fromVector Par myDims out
bloom :: Double -> Int -> Image U RGB Double -> IO (Image U RGB Double)
bloom strength divider img = do
let myDims@(_ :. w) = size img
blurred <- boxBlur (w `div` divider) 3 img
return . makeArrayR U Par myDims
$ \ix -> img `unsafeIndex` ix `add`
mul strength (blurred `unsafeIndex` ix)
supersample :: Image U RGB Double -> Image U RGB Double
supersample img = let
h :. w = size img
{-# INLINE pix #-}
pix y x = img `unsafeIndex` (y :. x)
{-# INLINE f #-}
f (y :. x) = mul 0.25
$ pix (2*y) (2*x) `add` pix (2*y+1) (2*x) `add` pix (2*y) (2*x+1)
`add` pix (2*y+1) (2*x+1)
in makeArrayR U Par (Sz ((h `div` 2) :. (w `div` 2))) f
================================================
FILE: src/Raytracer.hs
================================================
{-# LANGUAGE StrictData #-}
{-# LANGUAGE Strict #-}
module Raytracer (render, writeImg) where
import Linear hiding (lookAt, mult, trace)
import qualified Linear as L
import Control.Applicative
import Control.Lens
import Data.Default
import Data.Massiv.Array as A
import Data.Massiv.Array.IO
import Graphics.ColorSpace
import Prelude as P
import StarMap
import ConfigFile
import ImageFilters
data Layer = Layer (Pixel RGBA Double) | Bottom (Pixel RGBA Double) | None
data PhotonState = PhotonState (V3 Double) (V3 Double)
sRGB :: Double -> Double
sRGB x = let
a = 0.055
in if x < 0.0031308 then 12.92 * x
else (1 + a) * x ** (1.0 / 2.4) - a
writeImg :: Image U RGB Double -> FilePath -> IO ()
writeImg img path =
writeArray PNG def path
. A.map (toWord8 . fmap sRGB) $ img
blend :: Pixel RGBA Double -> Pixel RGBA Double -> Pixel RGBA Double
blend src@(PixelRGBA _ _ _ ta) = let
comp tc bc = tc + bc * (1 - ta)
in liftA2 comp src
-- Generate the sight rays ie. initial conditions for the integration
generateRay :: Config -> Ix2 -> PhotonState
generateRay cfg (y' :. x') = PhotonState vel pos
where cam = camera cfg
pos = position cam
scn = scene cfg
w = fromIntegral . fst $ resolution scn
h = fromIntegral . snd $ resolution scn
matr = L.lookAt pos (lookAt cam) (upVec cam) ^. _m33
vel = L.normalize . (L.transpose matr !*)
$ V3 (fov cam * (fromIntegral x' / w - 0.5))
(fov cam * (0.5 - fromIntegral y' / h) * h/w)
(-1)
render :: Config -> StarTree -> Image U RGB Double
render cfg startree = let
scn = scene cfg
cam = camera cfg
(w, h) = resolution scn
res@(w', h') = if supersampling scn then (2*w, 2*h) else (w, h)
scn' = scn { safeDistance =
max (50^(2 :: Int)) (2 * quadrance (position cam))
, diskInner = diskInner scn ^ (2 :: Int)
, diskOuter = diskOuter scn ^ (2 :: Int)
, resolution = res }
cfg' = cfg { scene = scn' }
diskRGB = toPixelRGB $ diskColor scn
img = makeArrayR U Par (h' :. w') $ traceRay cfg' diskRGB startree :: Image U RGB Double
in if supersampling scn then supersample img else img
traceRay :: Config -> Pixel RGB Double -> StarTree -> Ix2
-> Pixel RGB Double
traceRay cfg diskRGB startree pt = let
ray@(PhotonState vel pos) = generateRay cfg pt
h2 = quadrance $ pos `cross` vel
scn = scene cfg
in dropAlpha . colorize scn diskRGB startree h2 $ ray
colorize :: Scene -> Pixel RGB Double -> StarTree -> Double -> PhotonState
-> Pixel RGBA Double
colorize scn diskRGB startree h2 crd = let
colorize' rgba crd' = let
newCrd = rk4 (stepSize scn) h2 crd'
in case findColor scn diskRGB startree crd' newCrd of
Layer rgba' -> colorize' (blend rgba rgba') newCrd
Bottom rgba' -> blend rgba rgba'
None -> colorize' rgba newCrd
in colorize' (PixelRGBA 0 0 0 0) crd
findColor :: Scene -> Pixel RGB Double -> StarTree -> PhotonState -> PhotonState
-> Layer
{-# INLINE findColor #-}
findColor scn diskRGB startree (PhotonState vel pos@(V3 _ y _))
(PhotonState _ newPos@(V3 _ y' _))
| r2 < 1 = Bottom (PixelRGBA 0 0 0 1) -- already passed the event horizon
| r2 > safeDistance scn = Bottom . addAlpha 1.0
$ starLookup startree (starIntensity scn) (starSaturation scn) vel
| diskOpacity scn /= 0 && signum y' /= signum y
&& r2ave > diskInner scn && r2ave < diskOuter scn
= Layer $ diskColor' scn diskRGB (sqrt r2ave)
| otherwise = None
where r2 = quadrance pos
r2' = quadrance newPos
r2ave = (y'*r2 - y*r2') / (y' - y)
diskColor' :: Scene -> Pixel RGB Double -> Double -> Pixel RGBA Double
{-# INLINE diskColor' #-}
diskColor' scn diskRGB r = let
rInner = sqrt (diskInner scn)
rOuter = sqrt (diskOuter scn)
intensity = sin (pi * ((rOuter - r) / (rOuter - rInner)) ^ (2 :: Int))
rgb = fmap (* intensity) diskRGB
in addAlpha (intensity * diskOpacity scn) rgb
rk4 :: Double -> Double -> PhotonState -> PhotonState
{-# INLINE rk4 #-}
rk4 h h2 y = let
mul :: Double -> PhotonState -> PhotonState
{-# INLINE mul #-}
mul a (PhotonState u v) = PhotonState (u ^* a) (v ^* a)
add :: PhotonState -> PhotonState -> PhotonState
{-# INLINE add #-}
add (PhotonState x z) (PhotonState u v) = PhotonState (x ^+^ u) (z ^+^ v)
f :: PhotonState -> PhotonState
{-# INLINE f #-}
f (PhotonState vel pos) =
PhotonState (-1.5*h2 / (norm pos ^ (5 :: Int)) *^ pos) vel
k1 = f y
k2 = f $ y `add` mul (h / 2) k1
k3 = f $ y `add` mul (h / 2) k2
k4 = f $ y `add` mul h k3
sumK = k1 `add` mul 2 k2 `add` mul 2 k3 `add` k4
in y `add` mul (h / 6) sumK
================================================
FILE: src/StarMap.hs
================================================
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE Strict #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module StarMap
( Star, StarTree, StoredStarTree
, readMapFromFile, treeToByteString, readTreeFromFile
, buildStarTree, starLookup ) where
import Control.Monad
import Control.Applicative (liftA2)
import Data.Word
import Data.Char
import Data.Foldable
import qualified Data.ByteString as B
import Data.Serialize as S
import Data.KdMap.Static
import Linear as L
import Graphics.ColorSpace
import Util
type Star = (V3 Double, (Int, Double, Double))
type StarTree = KdMap Double (V3 Double) (Int, Double, Double)
type StoredStar = (V3 Double, (Int, Char))
type StoredStarTree = KdMap Double (V3 Double) (Int, Char)
instance Serialize StoredStarTree
instance Serialize (TreeNode Double (V3 Double) (Int, Char))
-- We can't serialize functions but let's hack around it so that we can
-- serialize the KdMap anyway
instance Serialize (SquaredDistanceFn Double (V3 Double)) where
put _ = put (0 :: Word8)
get = skip 1 >> return (defaultSqrDist toList)
instance Serialize (PointAsListFn Double (V3 Double)) where
put _ = put (0 :: Word8)
get = skip 1 >> return toList
-- Parse the star list in the binary format specified at
-- http://tdc-www.harvard.edu/software/catalogs/ppm.entry.html
readMap :: Get [StoredStar]
readMap = do
-- Skip the header
skip 28
nBytes <- remaining
replicateM (nBytes `div` 28) $ do
ra <- getFloat64be
dec <- getFloat64be
spectral <- getWord8
skip 1
mag <- getInt16be
skip 8
return ( raDecToCartesian ra dec
, (fromIntegral mag, chr $ fromIntegral spectral) )
starColor' :: (Int, Char) -> (Int, Double, Double)
starColor' (mag, ch) = let (h, s) = starColor ch in (mag, h, s)
-- Some nice colour values for different spectral types
starColor :: Char -> (Double, Double)
starColor 'O' = (0.631, 0.39)
starColor 'B' = (0.628, 0.33)
starColor 'A' = (0.622, 0.21)
starColor 'F' = (0.650, 0.03)
starColor 'G' = (0.089, 0.09)
starColor 'K' = (0.094, 0.29)
starColor 'M' = (0.094, 0.56)
starColor _ = (0, 0)
raDecToCartesian :: Double -> Double -> V3 Double
raDecToCartesian ra dec = V3 (cos dec*cos ra) (cos dec*sin ra) (sin dec)
readMapFromFile :: FilePath -> IO (Either String [StoredStar])
readMapFromFile path = do
ebs <- readSafe path
return $ ebs >>= runGet readMap
readTreeFromFile :: FilePath -> IO (Either String StarTree)
readTreeFromFile path = do
ebs <- readSafe path
return $ fmap starColor' <$> (S.decode =<< ebs)
treeToByteString :: StoredStarTree -> B.ByteString
treeToByteString = S.encode
buildStarTree :: [StoredStar] -> StoredStarTree
buildStarTree = build toList
starLookup :: StarTree -> Double -> Double -> V3 Double -> Pixel RGB Double
{-# INLINE starLookup #-}
starLookup starmap intensity saturation vel = let
-- The magnitude value tells about the intensity of the star. The
-- brighter the star, the smaller the magnitude. These constants are
-- used for adjusting the dynamics of the rendered celestial sphere.
max_brightness = 950 -- the "maximum brightness" magnitude
dynamic = 50 -- "dynamic range": magnitude change that doubles intensity
w = 0.0005 -- width parameter of the gaussian function
nvel = L.normalize vel
stars = inRadius starmap (3 * w) nvel
renderPixel (pos, (mag, hue, sat)) = let
d2 = qd pos nvel
a = log 2 / dynamic
-- Conversion from the log magnitude scale to linear brightness
-- and a Gaussian intensity function. This determines the apparent size
-- and brightness of the star.
val = (* intensity) . min 1
. exp $ a * (max_brightness - fromIntegral mag) - d2 / (2 * w^(2 :: Int))
in toPixelRGB $ PixelHSI hue (saturation * sat) val
in fmap (min 1) . foldl' (liftA2 (+)) (PixelRGB 0 0 0) $ renderPixel <$> stars
================================================
FILE: src/Util.hs
================================================
module Util ( promptOverwriteFile, readSafe, normalizePath
, timeAction, padZero ) where
import System.Directory
import System.IO
import qualified Data.ByteString as B
import Data.Time.Clock.POSIX (getPOSIXTime)
import System.FilePath
import Control.DeepSeq
readSafe :: FilePath -> IO (Either String B.ByteString)
readSafe path = do
exists <- doesFileExist path
if exists then Right <$> B.readFile path
else return . Left $ "Error: file " ++ path
++ " doesn't exist.\n"
promptOverwriteFile :: FilePath -> (FilePath -> IO ()) -> IO ()
promptOverwriteFile path doWrite = do
doesExist <- doesFileExist path
if doesExist then do
putStr $ "Overwrite " ++ path ++ "? [y/N] "
hFlush stdout
answer <- getLine
if answer == "y" || answer == "Y" then doWrite path
else putStrLn "Nothing was written."
else doWrite path
normalizePath :: FilePath -> IO FilePath
normalizePath path = (dropTrailingPathSeparator . normalise)
<$> makeRelativeToCurrentDirectory path
timeAction :: NFData a => String -> a -> IO a
timeAction actionName value = do
time1 <- (round <$> getPOSIXTime) :: IO Int
let res = value
time2 <- round <$> (res `deepseq` getPOSIXTime)
let secs = time2 - time1
putStrLn $ actionName ++ " completed in " ++ show (secs `div` 60)
++ " min " ++ show (secs `rem` 60) ++ " sec."
return res
padZero :: Int -> Int -> String
padZero maxVal val = let
nDigits x = (floor . logBase 10 $ (fromIntegral x :: Double)) + 1
nZeros = nDigits maxVal - nDigits val
zeros = replicate nZeros '0'
in zeros ++ show val
================================================
FILE: stack.yaml
================================================
resolver: lts-13.16
flags: {}
extra-package-dbs: []
packages:
- '.'
gitextract_5lpryipi/ ├── .gitignore ├── LICENSE ├── README.md ├── Setup.hs ├── animations/ │ └── default-ani.yaml ├── app/ │ ├── Animate.hs │ ├── GenerateTree.hs │ └── Main.hs ├── blackstar.cabal ├── scenes/ │ ├── closeup.yaml │ ├── default-aa.yaml │ ├── default.yaml │ ├── fartheraway.yaml │ ├── lensing-disk.yaml │ ├── lensing.yaml │ ├── wideangle-disk.yaml │ ├── wideangle.yaml │ └── wideangle1.yaml ├── scripts/ │ └── ffmpeg-animate ├── src/ │ ├── Animation.hs │ ├── ConfigFile.hs │ ├── ImageFilters.hs │ ├── Raytracer.hs │ ├── StarMap.hs │ └── Util.hs └── stack.yaml
Condensed preview — 26 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (47K chars).
[
{
"path": ".gitignore",
"chars": 143,
"preview": ".stack-work\nblackstar.prof\nout.png\ntexture.jpg\nBSC5\nSAO.pc\nPPM\nPPMra\nrenders\nbloomed.png\noutput\n*.png\n!example.png\nframe"
},
{
"path": "LICENSE",
"chars": 1527,
"preview": "Copyright Sakari Kapanen (c) 2016\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or with"
},
{
"path": "README.md",
"chars": 6398,
"preview": "# Blackstar\nA black hole ray tracer written in Haskell. There's [an article](https://flannelhead.github.io/projects/blac"
},
{
"path": "Setup.hs",
"chars": 46,
"preview": "import Distribution.Simple\nmain = defaultMain\n"
},
{
"path": "animations/default-ani.yaml",
"chars": 1147,
"preview": "# Animation files are quite similar to scene files. The difference is that you\n# can define multiple camera instances wh"
},
{
"path": "app/Animate.hs",
"chars": 2482,
"preview": "{-# LANGUAGE DeriveDataTypeable #-}\n{-# OPTIONS_GHC -fno-cse #-}\n\nmodule Main where\n\nimport Prelude hiding (writeFile)\ni"
},
{
"path": "app/GenerateTree.hs",
"chars": 1010,
"preview": "module Main where\n\nimport qualified Data.ByteString.Lazy as B\nimport System.Environment (getArgs)\n\nimport Util\nimport St"
},
{
"path": "app/Main.hs",
"chars": 4670,
"preview": "{-# LANGUAGE DeriveDataTypeable #-}\n{-# OPTIONS_GHC -fno-cse #-}\n\nmodule Main where\n\nimport System.Directory\nimport Cont"
},
{
"path": "blackstar.cabal",
"chars": 2526,
"preview": "name: blackstar\nversion: 0.1.0.0\nsynopsis: A black hole ray tracer\ndescription: "
},
{
"path": "scenes/closeup.yaml",
"chars": 270,
"preview": "camera:\n position: [10, 1, -2]\n lookAt: [0, 0, 6]\n upVec: [0, 1, 0]\n fov: 1.2\n\nscene:\n "
},
{
"path": "scenes/default-aa.yaml",
"chars": 334,
"preview": "camera:\n position: [0, 1, -20]\n lookAt: [2, 0, 0]\n upVec: [-0.2, 1, 0]\n fov: 1.5\n\nscene:\n "
},
{
"path": "scenes/default.yaml",
"chars": 1446,
"preview": "camera:\n # All the vectors are [x, y, z] coordinates\n position: [0, 1, -20] # The position of the camera\n lo"
},
{
"path": "scenes/fartheraway.yaml",
"chars": 303,
"preview": "camera:\n position: [-25, 1, -60]\n lookAt: [-12, -4, 0]\n upVec: [0.15, 1, 0]\n fov: 2\n\nscene"
},
{
"path": "scenes/lensing-disk.yaml",
"chars": 297,
"preview": "camera:\n position: [30, 0.4, 3]\n lookAt: [0, 0, 0]\n upVec: [0, 1, 0.2]\n fov: 1\n\nscene:\n "
},
{
"path": "scenes/lensing.yaml",
"chars": 295,
"preview": "camera:\n position: [30, 0.4, 3]\n lookAt: [0, 0, 0]\n upVec: [0, 1, 0.2]\n fov: 1\n\nscene:\n "
},
{
"path": "scenes/wideangle-disk.yaml",
"chars": 305,
"preview": "camera:\n position: [-6, 1, -20]\n lookAt: [-6, -4, 0]\n upVec: [-0.2, 1, 0]\n fov: 3.5\n\nscene"
},
{
"path": "scenes/wideangle.yaml",
"chars": 258,
"preview": "camera:\n position: [20, 0, 0]\n lookAt: [0, 0, 3.5]\n upVec: [0, 1, 0]\n fov: 2\n\nscene:\n r"
},
{
"path": "scenes/wideangle1.yaml",
"chars": 258,
"preview": "camera:\n position: [0, 0, 20]\n lookAt: [3.5, 0, 0]\n upVec: [0, 1, 0]\n fov: 2\n\nscene:\n r"
},
{
"path": "scripts/ffmpeg-animate",
"chars": 271,
"preview": "#!/bin/sh\n\n# Create YouTube quality video from stills using ffmpeg\n#\n# USAGE:\n# ffmpeg-animate PREFIX\n#\n# Writes to file"
},
{
"path": "src/Animation.hs",
"chars": 3289,
"preview": "{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveGeneric #-}\n\nmodule Animation ( Keyframe(camera, time)\n "
},
{
"path": "src/ConfigFile.hs",
"chars": 3041,
"preview": "{-# LANGUAGE OverloadedStrings, FlexibleInstances, DeriveGeneric #-}\n{-# OPTIONS_GHC -fno-warn-orphans #-}\n\nmodule Confi"
},
{
"path": "src/ImageFilters.hs",
"chars": 3365,
"preview": "{-# LANGUAGE BangPatterns #-}\n{-# LANGUAGE StrictData #-}\n{-# LANGUAGE Strict #-}\n\nmodule ImageFilters (bloom, supersamp"
},
{
"path": "src/Raytracer.hs",
"chars": 4882,
"preview": "{-# LANGUAGE StrictData #-}\n{-# LANGUAGE Strict #-}\n\nmodule Raytracer (render, writeImg) where\n\nimport Linear hiding (lo"
},
{
"path": "src/StarMap.hs",
"chars": 4001,
"preview": "{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}\n{-# LANGUAGE StrictData #-}\n{-# LANGUAGE Strict #-}\n\n{-# OPTION"
},
{
"path": "src/Util.hs",
"chars": 1696,
"preview": "module Util ( promptOverwriteFile, readSafe, normalizePath\n , timeAction, padZero ) where\n\nimport System.Dire"
},
{
"path": "stack.yaml",
"chars": 68,
"preview": "resolver: lts-13.16\nflags: {}\nextra-package-dbs: []\npackages:\n- '.'\n"
}
]
About this extraction
This page contains the full source code of the flannelhead/blackstar GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 26 files (43.3 KB), approximately 12.3k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.