From e0e718218433a484a6fe9c663eee25e5387d88e1 Mon Sep 17 00:00:00 2001 From: Charles Averill Date: Tue, 10 Oct 2023 02:37:28 -0500 Subject: [PATCH] Initial commit --- .github/workflows/ci.yml | 81 ++++++++++++++++++++++++++++++++++++++++ .gitignore | 11 ++++++ .ocamlformat | 1 + .vscode/settings.json | 6 +++ LICENSE | 21 +++++++++++ Makefile | 76 +++++++++++++++++++++++++++++++++++++ README.md | 37 ++++++++++++++++++ bin/dune | 4 ++ bin/main.ml | 28 ++++++++++++++ docs/dune | 3 ++ docs/zenith.mld | 3 ++ dune-project | 23 ++++++++++++ lib/constants.ml | 1 + lib/dune | 4 ++ lib/logging.ml | 81 ++++++++++++++++++++++++++++++++++++++++ lib/matrix.ml | 70 ++++++++++++++++++++++++++++++++++ lib/mesh.ml | 35 +++++++++++++++++ lib/render.ml | 43 +++++++++++++++++++++ lib/vector.ml | 12 ++++++ test/dune | 2 + test/owm.ml | 0 test/zenith.ml | 0 zenith.odocl | 1 + zenith.opam | 32 ++++++++++++++++ 24 files changed, 575 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .ocamlformat create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 bin/dune create mode 100644 bin/main.ml create mode 100644 docs/dune create mode 100644 docs/zenith.mld create mode 100644 dune-project create mode 100644 lib/constants.ml create mode 100644 lib/dune create mode 100644 lib/logging.ml create mode 100644 lib/matrix.ml create mode 100644 lib/mesh.ml create mode 100644 lib/render.ml create mode 100644 lib/vector.ml create mode 100644 test/dune create mode 100644 test/owm.ml create mode 100644 test/zenith.ml create mode 100644 zenith.odocl create mode 100644 zenith.opam diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..172baad --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,81 @@ +name: CI + +on: + push: + branches: [ main ] + +jobs: + build_test: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + + - name: Use OCaml 4.13.x + uses: ocaml/setup-ocaml@v2 + with: + ocaml-compiler: 4.13.x + + - name: Install Dependencies + run: | + opam install . --deps-only --with-test + eval $(opam env) + + - name: Build Emulator + run: make build + + - name: Run Unit Tests + run: make test + + build_docs: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + + - name: Use OCaml 4.13.x + uses: ocaml/setup-ocaml@v2 + with: + ocaml-compiler: 4.13.x + + - name: Install Dependencies + run: | + opam install . --deps-only --with-doc + eval $(opam env) + + - name: Build Documentation + run: | + make docs + + - name: Copy documentation to gh-pages branch + uses: actions/upload-artifact@v3 + with: + name: docs + path: docs + retention-days: 1 + + publish_docs: + runs-on: ubuntu-latest + needs: build_docs + + steps: + - uses: actions/checkout@v2 + with: + ref: gh-pages + + - name: Download documentation files + uses: actions/download-artifact@v2 + with: + name: docs + path: docs + + - name: Commit + uses: EndBug/add-and-commit@v9 + with: + author_name: Charles Averill + author_email: charlesaverill20@gmail.com + message: "Latest docs - ${{ github.event.repository.updated_at}}" + branch: gh-pages + add: '[./*]' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99f3798 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*~ +*.annot +*.cmo +*.cma +*.cmi +*.a +*.o +*.cmx +*.cmxs +*.cmxa +_build diff --git a/.ocamlformat b/.ocamlformat new file mode 100644 index 0000000..b73a05f --- /dev/null +++ b/.ocamlformat @@ -0,0 +1 @@ +version=0.25.1 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0efc035 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "ocaml.sandbox": { + "kind": "opam", + "switch": "/home/charles/_opam" + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4323fc6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Charles Averill + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8b6be71 --- /dev/null +++ b/Makefile @@ -0,0 +1,76 @@ +.PHONY: default build install uninstall test clean fmt +.IGNORE: fmt + +default: build + +fmt: + opam exec -- dune build @fmt + opam exec -- dune promote + +build: fmt + opam exec -- dune build + +install: + opam exec -- dune install + +uninstall: + opam exec -- dune uninstall + +clean: + opam exec -- dune clean + git clean -dfXq + +test: fmt + opam exec -- dune runtest + +testf: fmt + opam exec -- dune runtest -f + +run: build + opam exec -- dune exec -- zenith + +raw_run: build + clear + _build/default/bin/main.exe + +debug: build + opam exec -- ocamldebug _build/default/zenith/main.bc + +DOCS_PATH=docs/ +DOCS_NAME=zenith +DOCS_DESCR=Zen Engine for Navigating wIreframes In Three-dimensional Holographic space +DOCS_INDEX_TITLE=$(DOCS_NAME) - $(DOCS_DESCR) +define DOCS_EMBED +\ +\ + +endef + +cleandocs: + if [ ! -d $(DOCS_PATH) ]; then \ + mkdir $(DOCS_PATH); \ + fi + rm -rf $(DOCS_PATH)module $(DOCS_PATH)docs $(DOCS_PATH)odoc.support $(DOCS_PATH)index.html + +docs: cleandocs build + opam exec -- dune build @doc + mv -f _build/default/_doc/_html/* $(DOCS_PATH) + rm -f $(DOCS_PATH)index.html + mv $(DOCS_PATH)zenith/zenith.html $(DOCS_PATH)index.html + mv $(DOCS_PATH)zenith $(DOCS_PATH)module + + @echo "Preparing Index\n--------------" + # Header + sed -i 's/.*<\/title>/<title>$(DOCS_INDEX_TITLE)<\/title>/g' $(DOCS_PATH)index.html + sed -i 's@</head>@$(DOCS_EMBED)\n</head>@g' $(DOCS_PATH)index.html + sed -i 's/..\/odoc.support/odoc.support/g' $(DOCS_PATH)index.html + # Body + sed -i "s@<nav class="odoc-nav">.*gbcamel</nav>@@g" $(DOCS_PATH)index.html + +push: cleandocs build + @read -p "Commit message: " input; \ + if [ -z "$input" ]; then \ + echo "Error: Please provide a valid commit message."; \ + exit 1; \ + fi; \ + git add . && git commit -m "$$input" && git push origin main diff --git a/README.md b/README.md new file mode 100644 index 0000000..9462581 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Friendly OCaml Template + +Just run `setup.sh` to set up a ready-for-development OCaml environment + +## Warning +Project description and synopsis should not include single quotes + +## Docs note +Once you modify `docs/PROJECT_NAME.mld` to contain multiple sections, the `up - PROJECT_NAME` navigation at the top of the index page will disappear, and a navigation sidebar will replace it + +## License +The default license generated is MIT, but feel free to change it. <b>THIS TEMPLATE IS LICENSED UNDER THE UNLICENSE</b> + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to <https://unlicense.org> diff --git a/bin/dune b/bin/dune new file mode 100644 index 0000000..8345979 --- /dev/null +++ b/bin/dune @@ -0,0 +1,4 @@ +(executable + (public_name zenith) + (name main) + (libraries zenith graphics)) diff --git a/bin/main.ml b/bin/main.ml new file mode 100644 index 0000000..8fe8822 --- /dev/null +++ b/bin/main.ml @@ -0,0 +1,28 @@ +open Graphics +open Zenith.Mesh +open Zenith.Render +open List + +let clear_window color = + let fg = foreground in + set_color color; + fill_rect 0 0 (size_x ()) (size_y ()); + set_color fg + +let break_mainloop = ref false + +let draw_scene () = + clear_window black; + set_color white; + draw_meshes (project_clip_meshes (size_x ()) (size_y ()) 0. 10. [ cube ]); + let input = wait_next_event [ Mouse_motion; Button_down ] in + synchronize (); + if input.button then break_mainloop := true else () + +let rec main_loop () = + draw_scene (); + if !break_mainloop then () else main_loop () + +let () = + open_graph " 800x600"; + main_loop () diff --git a/docs/dune b/docs/dune new file mode 100644 index 0000000..8bdfedc --- /dev/null +++ b/docs/dune @@ -0,0 +1,3 @@ +(documentation + (package zenith) + (mld_files zenith)) diff --git a/docs/zenith.mld b/docs/zenith.mld new file mode 100644 index 0000000..afecce9 --- /dev/null +++ b/docs/zenith.mld @@ -0,0 +1,3 @@ +{0 Index} + +Hello World! diff --git a/dune-project b/dune-project new file mode 100644 index 0000000..c69cf37 --- /dev/null +++ b/dune-project @@ -0,0 +1,23 @@ +(lang dune 3.9) + +(name zenith) + +(generate_opam_files true) + +(source + (github CharlesAverill/zenith)) + +(authors "Charles Averill") + +(maintainers "Charles Averill") + +(license LICENSE) + +(documentation https://github.com/CharlesAverill/zenith) + +(package + (name zenith) + (synopsis "A wireframe renderer") + (description "Zen Engine for Navigating wIreframes In Three-dimensional Holographic space") + (depends ocaml dune graphics)) + diff --git a/lib/constants.ml b/lib/constants.ml new file mode 100644 index 0000000..c8cbe84 --- /dev/null +++ b/lib/constants.ml @@ -0,0 +1 @@ +let pi = 3.141592 diff --git a/lib/dune b/lib/dune new file mode 100644 index 0000000..82cf1da --- /dev/null +++ b/lib/dune @@ -0,0 +1,4 @@ +(library + (name zenith) + (modules render vector matrix mesh constants logging) + (libraries graphics)) diff --git a/lib/logging.ml b/lib/logging.ml new file mode 100644 index 0000000..b578c7a --- /dev/null +++ b/lib/logging.ml @@ -0,0 +1,81 @@ +(** logging.ml - Custom logging and error messages *) + +(** Represents the severity of a log statement *) +type log_type = + | Log_None + | Log_Debug + | Log_Info + | Log_Warning + | Log_Error + | Log_Critical + +let _GLOBAL_LOG_LEVEL = Log_Info + +(** Follows the order in the type definition, \[0:5\]*) +let int_of_log = function + | Log_Debug -> 1 + | Log_Info -> 2 + | Log_Warning -> 3 + | Log_Error -> 4 + | Log_Critical -> 5 + | Log_None -> 0 + +type return_code = int * string +(** For exits, their appropriate return code and the message type *) + +let rc_Ok = (0, "OK") +and rc_Error = (1, "ERROR") +and rc_OOB = (2, "OUT OF BOUNDS ERROR") + +(** ANSI encoding for bold text *) +let ansi_bold = "\x1b[1m" + +(** ANSI encoding for red text *) +let ansi_red = "\x1b[38:5:196m" + +(** ANSI encoding for orange text *) +let ansi_orange = "\x1b[38:5:208m" + +(** ANSI encoding for yellow text *) +let ansi_yellow = "\x1b[38:5:178m" + +(** ANSI encoding for plain text *) +let ansi_reset = "\x1b[0m" + +(** ANSI encoding for bold red text *) +let error_red = ansi_bold ^ ansi_red + +(** ANSI encoding for bold orange text *) +let error_orange = ansi_bold ^ ansi_orange + +(** ANSI encoding for bold yellow text *) +let error_yellow = ansi_bold ^ ansi_yellow + +(** Gets the string representation of a {!log_type}*) +let string_of_log = function + | Log_Debug -> ansi_bold ^ "[DEBUG]" + | Log_Info -> ansi_bold ^ "[INFO]" + | Log_Warning -> ansi_yellow ^ "[WARNING]" + | Log_Error -> ansi_orange ^ "[ERROR]" + | Log_Critical -> ansi_red ^ "[CRITICAL]" + | Log_None -> ansi_reset ^ "[NONE]" + +(** A fatal log statement that immediately exits the program *) +let fatal rc message = + Printf.fprintf stderr + "%s[%s] - %s%s\n----------------------------------------\n" error_red + (snd rc) ansi_reset message; + flush stderr; + exit (fst rc) + +(** Prints log statements to stdout/stderr *) +let _log log_level message = + if log_level = Log_None || int_of_log _GLOBAL_LOG_LEVEL > int_of_log log_level + then () + else + let stream = + if log_level = Log_Debug || log_level = Log_Info then stdout else stderr + in + Printf.fprintf stream "LOG:%s%s - %s\n" (string_of_log log_level) ansi_reset + message; + flush stream diff --git a/lib/matrix.ml b/lib/matrix.ml new file mode 100644 index 0000000..62c668d --- /dev/null +++ b/lib/matrix.ml @@ -0,0 +1,70 @@ +open Logging +open Vector + +type matrix = { arr : float array; dim : int * int } + +let get_matrix n m = { arr = Array.make (n * m) 0.0; dim = (n, m) } +let xdim m = fst m.dim +let ydim m = snd m.dim +let xy_to_i m x y = (x * xdim m) + y +let get mat n m = Array.get mat.arr (xy_to_i mat n m) +let set mat n m x = Array.set mat.arr (xy_to_i mat n m) x + +let id_matrix n m = + let mat = get_matrix n m in + List.fold_left + (fun _ i -> if i < n && i < m then set mat i i 1. else ()) + () + (List.init (max n m) (fun x -> x)); + mat + +let print_matrix mat = + print_string "["; + for x = 0 to xdim mat - 1 do + if x != 0 then print_string " "; + print_string "["; + for y = 0 to ydim mat - 1 do + print_float (get mat x y); + print_string ";"; + if y + 1 != ydim mat then print_string " " + done; + print_string "]"; + if x + 1 != xdim mat then print_endline "" + done; + print_endline "]" + +let matmul a b = + if xdim a != ydim a || a.dim != b.dim then + fatal rc_OOB "Can only matmul square matrices of identical shape"; + let dim = xdim a in + let dest = id_matrix dim dim in + for y = 0 to dim - 1 do + let col = y * dim in + for x = 0 to dim - 1 do + for i = 0 to dim - 1 do + Array.set dest.arr (col + x) + (Array.get b.arr (i + col) *. Array.get a.arr (x + (i * 4))) + done + done + done; + dest + +let vecmatmul v m = + let arr = Array.get m.arr in + { + x = vec_dot v { x = arr 0; y = arr 4; z = arr 8; w = arr 12 }; + y = vec_dot v { x = arr 1; y = arr 5; z = arr 9; w = arr 13 }; + z = vec_dot v { x = arr 2; y = arr 6; z = arr 10; w = arr 14 }; + w = vec_dot v { x = arr 3; y = arr 7; z = arr 11; w = arr 15 }; + } + +let clip_matrix fov aspect_ratio near far = + let out = id_matrix 4 4 in + let f = 1. /. Float.tan (fov *. 0.5) in + set out 0 0 (f *. aspect_ratio); + set out 1 1 f; + set out 2 2 ((far +. near) /. (far -. near)); + set out 2 3 1.; + set out 3 2 (2. *. near *. far /. (near -. far)); + set out 3 3 0.; + out diff --git a/lib/mesh.ml b/lib/mesh.ml new file mode 100644 index 0000000..01adc90 --- /dev/null +++ b/lib/mesh.ml @@ -0,0 +1,35 @@ +open Vector + +type mesh = vec list * (int * int) list + +let map_verts mesh f = (List.map f (fst mesh), snd mesh) + +let cube = + ( [ + v3 0. 0. 0.; + v3 0. 0. 1.; + v3 0. 1. 0.; + v3 0. 1. 1.; + v3 1. 0. 0.; + v3 1. 0. 1.; + v3 1. 1. 0.; + v3 1. 1. 1.; + ], + [ + (0, 1); + (0, 2); + (0, 4); + (1, 3); + (1, 5); + (2, 3); + (2, 6); + (3, 7); + (4, 5); + (4, 6); + (5, 7); + (6, 7); + ] ) + +let pyramid = + ( [ v3 0. 0. 0.; v3 1. 0. 0.; v3 0. 1. 0.; v3 1. 1. 0.; v3 0.5 0.5 1. ], + [ (0, 1); (1, 2); (2, 3); (3, 0); (0, 4); (1, 4); (2, 4); (3, 4) ] ) diff --git a/lib/render.ml b/lib/render.ml new file mode 100644 index 0000000..c3839a9 --- /dev/null +++ b/lib/render.ml @@ -0,0 +1,43 @@ +open Constants +open Graphics +open Matrix +open Mesh +open Vector + +let project_clip_meshes w h near far meshes = + let _halfw, _halfh, aspect = + ( float_of_int w *. 0.5, + float_of_int h *. 0.5, + float_of_int w /. float_of_int h ) + in + let clip = clip_matrix (60. *. pi /. 180.) aspect near far in + List.fold_left + (fun outlist mesh -> + let mesh' = map_verts mesh (fun v -> vecmatmul v clip) in + mesh' :: outlist) + [] meshes + +let draw_meshes (meshes : mesh list) = + List.fold_left + (fun _ mesh -> + let verts, edges = (fst mesh, snd mesh) in + let _ = + List.map + (fun edge -> + let vert_a, vert_b = + (List.nth verts (fst edge), List.nth verts (snd edge)) + in + (* print_endline + ("Drawing " ^ string_of_float vert_a.x ^ " " + ^ string_of_float vert_a.y); *) + moveto + (50 + int_of_float (100. *. vert_a.x)) + (50 + int_of_float (100. *. vert_a.y)); + lineto + (50 + int_of_float (100. *. vert_b.x)) + (50 + int_of_float (100. *. vert_b.y)); + ()) + edges + in + ()) + () meshes diff --git a/lib/vector.ml b/lib/vector.ml new file mode 100644 index 0000000..1b64d65 --- /dev/null +++ b/lib/vector.ml @@ -0,0 +1,12 @@ +type vec = { x : float; y : float; z : float; w : float } + +let v3 x y z = { x; y; z; w = 0. } +let zvec = { x = 0.; y = 0.; z = 0.; w = 1. } +let vec_length vec = (vec.x *. vec.x) +. (vec.y *. vec.y) +. (vec.z *. vec.z) + +let vec_unit vec = + let len = vec_length vec in + { x = vec.x /. len; y = vec.y /. len; z = vec.z /. len; w = vec.w /. len } + +let vec_dot a b = (a.x *. b.x) +. (a.y *. b.y) +. (a.z *. b.z) +. (a.w *. b.w) +let vec_div v n = { x = v.x /. n; y = v.y /. n; z = v.y /. n; w = v.w } diff --git a/test/dune b/test/dune new file mode 100644 index 0000000..887211e --- /dev/null +++ b/test/dune @@ -0,0 +1,2 @@ +(test + (name zenith)) diff --git a/test/owm.ml b/test/owm.ml new file mode 100644 index 0000000..e69de29 diff --git a/test/zenith.ml b/test/zenith.ml new file mode 100644 index 0000000..e69de29 diff --git a/zenith.odocl b/zenith.odocl new file mode 100644 index 0000000..8c7081a --- /dev/null +++ b/zenith.odocl @@ -0,0 +1 @@ +dir: ./docs diff --git a/zenith.opam b/zenith.opam new file mode 100644 index 0000000..8a11305 --- /dev/null +++ b/zenith.opam @@ -0,0 +1,32 @@ +# This file is generated by dune, edit dune-project instead +opam-version: "2.0" +synopsis: "A wireframe renderer" +description: + "Zen Engine for Navigating wIreframes In Three-dimensional Holographic space" +maintainer: ["Charles Averill"] +authors: ["Charles Averill"] +license: "LICENSE" +homepage: "https://github.com/CharlesAverill/zenith" +doc: "https://github.com/CharlesAverill/zenith" +bug-reports: "https://github.com/CharlesAverill/zenith/issues" +depends: [ + "ocaml" + "dune" {>= "3.9"} + "graphics" + "odoc" {with-doc} +] +build: [ + ["dune" "subst"] {dev} + [ + "dune" + "build" + "-p" + name + "-j" + jobs + "@install" + "@runtest" {with-test} + "@doc" {with-doc} + ] +] +dev-repo: "git+https://github.com/CharlesAverill/zenith.git"