rednoise 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rednoise-0.1.2/.gitignore +6 -0
- rednoise-0.1.2/CMakeLists.txt +35 -0
- rednoise-0.1.2/PKG-INFO +126 -0
- rednoise-0.1.2/README.md +105 -0
- rednoise-0.1.2/example.py +53 -0
- rednoise-0.1.2/pyproject.toml +70 -0
- rednoise-0.1.2/rednoise/__init__.py +456 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# CMake driver for the RedNoise Python wheel (scikit-build-core).
|
|
2
|
+
#
|
|
3
|
+
# This does NOT build a Python extension module. The binding is pure ctypes; all
|
|
4
|
+
# we need is the native shared library. So we pull in the parent C++ project and
|
|
5
|
+
# build ONLY the installable `rednoise` library (its stable C ABI), then install
|
|
6
|
+
# the resulting .so / .dll / .dylib INTO the importable `rednoise/` package dir
|
|
7
|
+
# so the wheel carries it and __init__.py finds it next to the package.
|
|
8
|
+
|
|
9
|
+
cmake_minimum_required(VERSION 3.24)
|
|
10
|
+
project(rednoise_python LANGUAGES C CXX)
|
|
11
|
+
|
|
12
|
+
# Build only the library from the parent project. Force these into the cache
|
|
13
|
+
# BEFORE add_subdirectory so the parent's option() calls pick them up. We keep
|
|
14
|
+
# the app (SDL3), tests, headless tools, GPU and Vulkan paths off, and disable
|
|
15
|
+
# fetching so the build is offline (glm is vendored in third_party/).
|
|
16
|
+
set(BUILD_APP OFF CACHE BOOL "" FORCE)
|
|
17
|
+
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
|
18
|
+
set(BUILD_HEADLESS OFF CACHE BOOL "" FORCE)
|
|
19
|
+
set(BUILD_GPU OFF CACHE BOOL "" FORCE)
|
|
20
|
+
set(BUILD_VULKAN OFF CACHE BOOL "" FORCE)
|
|
21
|
+
set(BUILD_LIB ON CACHE BOOL "" FORCE)
|
|
22
|
+
set(FETCH_DEPENDENCIES OFF CACHE BOOL "" FORCE)
|
|
23
|
+
# ctypes loads a SHARED library, so build rednoise as a .dll/.so/.dylib (with the
|
|
24
|
+
# C ABI exported - see WINDOWS_EXPORT_ALL_SYMBOLS on the target), not a static lib.
|
|
25
|
+
set(BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE)
|
|
26
|
+
|
|
27
|
+
# The C++ project root is two levels up (bindings/python -> repo root).
|
|
28
|
+
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../.. rednoise_root)
|
|
29
|
+
|
|
30
|
+
# Install the built native library into the importable package directory so it
|
|
31
|
+
# ships inside the wheel right next to rednoise/__init__.py.
|
|
32
|
+
install(TARGETS rednoise
|
|
33
|
+
RUNTIME DESTINATION rednoise
|
|
34
|
+
LIBRARY DESTINATION rednoise
|
|
35
|
+
ARCHIVE DESTINATION rednoise)
|
rednoise-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: rednoise
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Python ctypes bindings for the RedNoise CPU software renderer, with the native library bundled.
|
|
5
|
+
Keywords: renderer,raytracer,pathtracer,graphics,ctypes
|
|
6
|
+
Author: RedNoise contributors
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Topic :: Multimedia :: Graphics :: 3D Rendering
|
|
14
|
+
Project-URL: Homepage, https://github.com/ReverseZoom2151/rednoise
|
|
15
|
+
Project-URL: Repository, https://github.com/ReverseZoom2151/rednoise
|
|
16
|
+
Project-URL: Issues, https://github.com/ReverseZoom2151/rednoise/issues
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Provides-Extra: pillow
|
|
19
|
+
Requires-Dist: Pillow>=8.0; extra == "pillow"
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# RedNoise Python bindings
|
|
23
|
+
|
|
24
|
+
Python `ctypes` bindings for the RedNoise CPU software renderer. They call the
|
|
25
|
+
stable C ABI declared in `include/rednoise/rednoise.h`. There is no compiled
|
|
26
|
+
extension module: the bindings load the RedNoise shared library at runtime. The
|
|
27
|
+
native library is built and bundled into the package, so a normal install is
|
|
28
|
+
self-contained.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
pip install rednoise
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Prebuilt wheels ship the native library, so on a supported platform this is all
|
|
37
|
+
you need. Installing from an sdist compiles the native library on your machine
|
|
38
|
+
via CMake, so you need a C++23-capable compiler (GCC 12+, Clang 15+, or MSVC
|
|
39
|
+
2022+) and CMake 3.24 or newer. The build uses the vendored `glm` in
|
|
40
|
+
`third_party/` and runs fully offline; it does not fetch SDL3 or any other
|
|
41
|
+
optional dependency.
|
|
42
|
+
|
|
43
|
+
Under the hood, `pip install` drives scikit-build-core and CMake to build only
|
|
44
|
+
the installable `rednoise` library (`-DBUILD_LIB=ON`, everything else off) and
|
|
45
|
+
installs the resulting shared library into the importable `rednoise/` package:
|
|
46
|
+
|
|
47
|
+
- Linux: `librednoise.so`
|
|
48
|
+
- macOS: `librednoise.dylib`
|
|
49
|
+
- Windows: `rednoise.dll`
|
|
50
|
+
|
|
51
|
+
Pillow is optional and only needed for `Image.to_pillow()` or saving non-PNG
|
|
52
|
+
formats:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
pip install "rednoise[pillow]"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Library discovery
|
|
59
|
+
|
|
60
|
+
The bundled library is found automatically. For development against a
|
|
61
|
+
separately built library, the bindings locate it in this order:
|
|
62
|
+
|
|
63
|
+
1. The library bundled next to the package (what a wheel installs).
|
|
64
|
+
2. The `REDNOISE_LIBRARY` environment variable (an explicit path to the file).
|
|
65
|
+
3. The system search path (`ctypes.util.find_library("rednoise")`).
|
|
66
|
+
4. Common local build paths (`build/`, next to the package, and similar).
|
|
67
|
+
|
|
68
|
+
To override with a locally built library, set `REDNOISE_LIBRARY`:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
# Linux
|
|
72
|
+
export REDNOISE_LIBRARY="$PWD/build/librednoise.so"
|
|
73
|
+
|
|
74
|
+
# macOS
|
|
75
|
+
export REDNOISE_LIBRARY="$PWD/build/librednoise.dylib"
|
|
76
|
+
|
|
77
|
+
# Windows (cmd)
|
|
78
|
+
set REDNOISE_LIBRARY=%CD%\build\rednoise.dll
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Develop from a local checkout
|
|
82
|
+
|
|
83
|
+
Install the bindings straight from this directory:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
pip install ./bindings/python
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Add the Pillow extra with:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
pip install "./bindings/python[pillow]"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
You can still build the shared library by hand from the repository root and
|
|
96
|
+
point `REDNOISE_LIBRARY` at it:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
cmake -B build -S . -DBUILD_LIB=ON -DBUILD_APP=OFF
|
|
100
|
+
cmake --build build
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Usage
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import rednoise
|
|
107
|
+
|
|
108
|
+
print(rednoise.version())
|
|
109
|
+
|
|
110
|
+
with rednoise.Scene.load_obj("assets/cornell-box.obj") as scene:
|
|
111
|
+
print("triangles:", scene.triangle_count)
|
|
112
|
+
image = scene.render(mode="pathtraced", width=400, height=300, samples=64)
|
|
113
|
+
image.save("cornell.png")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`Scene.render` accepts a render mode as a `RenderMode` enum, an integer, or a
|
|
117
|
+
name string: `"wireframe"`, `"rasterised"`, `"raytraced"`, or `"pathtraced"`.
|
|
118
|
+
The returned `Image` exposes `.rgba` (raw RGBA8 bytes), `.size`, `.save(path)`,
|
|
119
|
+
and `.to_pillow()` when Pillow is installed.
|
|
120
|
+
|
|
121
|
+
A complete runnable example lives in `example.py`. Run it from the repository
|
|
122
|
+
root (after building the library and setting `REDNOISE_LIBRARY`):
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
python bindings/python/example.py
|
|
126
|
+
```
|
rednoise-0.1.2/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# RedNoise Python bindings
|
|
2
|
+
|
|
3
|
+
Python `ctypes` bindings for the RedNoise CPU software renderer. They call the
|
|
4
|
+
stable C ABI declared in `include/rednoise/rednoise.h`. There is no compiled
|
|
5
|
+
extension module: the bindings load the RedNoise shared library at runtime. The
|
|
6
|
+
native library is built and bundled into the package, so a normal install is
|
|
7
|
+
self-contained.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
pip install rednoise
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Prebuilt wheels ship the native library, so on a supported platform this is all
|
|
16
|
+
you need. Installing from an sdist compiles the native library on your machine
|
|
17
|
+
via CMake, so you need a C++23-capable compiler (GCC 12+, Clang 15+, or MSVC
|
|
18
|
+
2022+) and CMake 3.24 or newer. The build uses the vendored `glm` in
|
|
19
|
+
`third_party/` and runs fully offline; it does not fetch SDL3 or any other
|
|
20
|
+
optional dependency.
|
|
21
|
+
|
|
22
|
+
Under the hood, `pip install` drives scikit-build-core and CMake to build only
|
|
23
|
+
the installable `rednoise` library (`-DBUILD_LIB=ON`, everything else off) and
|
|
24
|
+
installs the resulting shared library into the importable `rednoise/` package:
|
|
25
|
+
|
|
26
|
+
- Linux: `librednoise.so`
|
|
27
|
+
- macOS: `librednoise.dylib`
|
|
28
|
+
- Windows: `rednoise.dll`
|
|
29
|
+
|
|
30
|
+
Pillow is optional and only needed for `Image.to_pillow()` or saving non-PNG
|
|
31
|
+
formats:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
pip install "rednoise[pillow]"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Library discovery
|
|
38
|
+
|
|
39
|
+
The bundled library is found automatically. For development against a
|
|
40
|
+
separately built library, the bindings locate it in this order:
|
|
41
|
+
|
|
42
|
+
1. The library bundled next to the package (what a wheel installs).
|
|
43
|
+
2. The `REDNOISE_LIBRARY` environment variable (an explicit path to the file).
|
|
44
|
+
3. The system search path (`ctypes.util.find_library("rednoise")`).
|
|
45
|
+
4. Common local build paths (`build/`, next to the package, and similar).
|
|
46
|
+
|
|
47
|
+
To override with a locally built library, set `REDNOISE_LIBRARY`:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
# Linux
|
|
51
|
+
export REDNOISE_LIBRARY="$PWD/build/librednoise.so"
|
|
52
|
+
|
|
53
|
+
# macOS
|
|
54
|
+
export REDNOISE_LIBRARY="$PWD/build/librednoise.dylib"
|
|
55
|
+
|
|
56
|
+
# Windows (cmd)
|
|
57
|
+
set REDNOISE_LIBRARY=%CD%\build\rednoise.dll
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Develop from a local checkout
|
|
61
|
+
|
|
62
|
+
Install the bindings straight from this directory:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
pip install ./bindings/python
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Add the Pillow extra with:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
pip install "./bindings/python[pillow]"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
You can still build the shared library by hand from the repository root and
|
|
75
|
+
point `REDNOISE_LIBRARY` at it:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
cmake -B build -S . -DBUILD_LIB=ON -DBUILD_APP=OFF
|
|
79
|
+
cmake --build build
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import rednoise
|
|
86
|
+
|
|
87
|
+
print(rednoise.version())
|
|
88
|
+
|
|
89
|
+
with rednoise.Scene.load_obj("assets/cornell-box.obj") as scene:
|
|
90
|
+
print("triangles:", scene.triangle_count)
|
|
91
|
+
image = scene.render(mode="pathtraced", width=400, height=300, samples=64)
|
|
92
|
+
image.save("cornell.png")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`Scene.render` accepts a render mode as a `RenderMode` enum, an integer, or a
|
|
96
|
+
name string: `"wireframe"`, `"rasterised"`, `"raytraced"`, or `"pathtraced"`.
|
|
97
|
+
The returned `Image` exposes `.rgba` (raw RGBA8 bytes), `.size`, `.save(path)`,
|
|
98
|
+
and `.to_pillow()` when Pillow is installed.
|
|
99
|
+
|
|
100
|
+
A complete runnable example lives in `example.py`. Run it from the repository
|
|
101
|
+
root (after building the library and setting `REDNOISE_LIBRARY`):
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
python bindings/python/example.py
|
|
105
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Runnable example for the RedNoise Python bindings.
|
|
2
|
+
|
|
3
|
+
This loads the Cornell box scene, path-traces it, saves a PNG, and prints the
|
|
4
|
+
triangle count and native library version.
|
|
5
|
+
|
|
6
|
+
Prerequisites:
|
|
7
|
+
1. Build the shared library from the project root:
|
|
8
|
+
cmake -B build -S . -DBUILD_LIB=ON -DBUILD_APP=OFF
|
|
9
|
+
cmake --build build
|
|
10
|
+
2. Point the binding at the built library (adjust the extension for your OS):
|
|
11
|
+
export REDNOISE_LIBRARY="$PWD/build/librednoise.so" # Linux
|
|
12
|
+
export REDNOISE_LIBRARY="$PWD/build/librednoise.dylib" # macOS
|
|
13
|
+
set REDNOISE_LIBRARY=%CD%\\build\\rednoise.dll # Windows
|
|
14
|
+
|
|
15
|
+
Run from the repository root so the asset path resolves:
|
|
16
|
+
python bindings/python/example.py
|
|
17
|
+
|
|
18
|
+
Or install the package first (pip install ./bindings/python) and run from
|
|
19
|
+
anywhere; this script locates assets relative to the repo layout.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
import rednoise
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> None:
|
|
28
|
+
# Resolve assets/cornell-box.obj relative to this file's location in the
|
|
29
|
+
# repo (bindings/python/example.py -> <repo>/assets/cornell-box.obj),
|
|
30
|
+
# falling back to a path relative to the current working directory.
|
|
31
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
32
|
+
repo_root = os.path.dirname(os.path.dirname(here))
|
|
33
|
+
obj_path = os.path.join(repo_root, "assets", "cornell-box.obj")
|
|
34
|
+
if not os.path.isfile(obj_path):
|
|
35
|
+
obj_path = os.path.join("assets", "cornell-box.obj")
|
|
36
|
+
|
|
37
|
+
print(f"RedNoise version: {rednoise.version()}")
|
|
38
|
+
|
|
39
|
+
with rednoise.Scene.load_obj(obj_path) as scene:
|
|
40
|
+
print(f"Triangle count: {scene.triangle_count}")
|
|
41
|
+
image = scene.render(
|
|
42
|
+
mode="pathtraced",
|
|
43
|
+
width=400,
|
|
44
|
+
height=300,
|
|
45
|
+
samples=64,
|
|
46
|
+
)
|
|
47
|
+
out_path = "cornell.png"
|
|
48
|
+
image.save(out_path)
|
|
49
|
+
print(f"Saved {image.width}x{image.height} render to {out_path}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
main()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Packaging metadata for the RedNoise Python bindings.
|
|
2
|
+
#
|
|
3
|
+
# This package builds and bundles the RedNoise shared library. scikit-build-core
|
|
4
|
+
# drives CMake to compile the installable `rednoise` library (C ABI) from the
|
|
5
|
+
# parent C++ project and installs the resulting .so / .dll / .dylib into the
|
|
6
|
+
# importable `rednoise/` package directory. Prebuilt wheels ship the library;
|
|
7
|
+
# building from an sdist requires a C++23-capable compiler and CMake >= 3.24.
|
|
8
|
+
|
|
9
|
+
[build-system]
|
|
10
|
+
requires = ["scikit-build-core>=0.8"]
|
|
11
|
+
build-backend = "scikit_build_core.build"
|
|
12
|
+
|
|
13
|
+
[project]
|
|
14
|
+
name = "rednoise"
|
|
15
|
+
version = "0.1.2"
|
|
16
|
+
description = "Python ctypes bindings for the RedNoise CPU software renderer, with the native library bundled."
|
|
17
|
+
readme = "README.md"
|
|
18
|
+
requires-python = ">=3.8"
|
|
19
|
+
license = { text = "MIT" }
|
|
20
|
+
authors = [
|
|
21
|
+
{ name = "RedNoise contributors" },
|
|
22
|
+
]
|
|
23
|
+
keywords = ["renderer", "raytracer", "pathtracer", "graphics", "ctypes"]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.8",
|
|
30
|
+
"Topic :: Multimedia :: Graphics :: 3D Rendering",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# No hard runtime dependencies: the binding is pure ctypes over the C ABI.
|
|
34
|
+
dependencies = []
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
# Pillow enables Image.to_pillow() and saving non-PNG formats.
|
|
38
|
+
pillow = ["Pillow>=8.0"]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/ReverseZoom2151/rednoise"
|
|
42
|
+
Repository = "https://github.com/ReverseZoom2151/rednoise"
|
|
43
|
+
Issues = "https://github.com/ReverseZoom2151/rednoise/issues"
|
|
44
|
+
|
|
45
|
+
[tool.scikit-build]
|
|
46
|
+
minimum-version = "0.8"
|
|
47
|
+
cmake.version = ">=3.24"
|
|
48
|
+
wheel.packages = ["rednoise"]
|
|
49
|
+
# The binding is pure ctypes loading a bundled shared library - there is no
|
|
50
|
+
# Python C extension, so the wheel is platform-specific but Python-version
|
|
51
|
+
# independent. Tag it py3-none-<platform> so ONE wheel per OS serves all of
|
|
52
|
+
# Python 3 (and the build runs once per OS instead of once per version).
|
|
53
|
+
wheel.py-api = "py3"
|
|
54
|
+
|
|
55
|
+
# cibuildwheel configuration (read from this file when building wheels). Build one
|
|
56
|
+
# wheel per OS for CPython 3.12 (the lib is py-version independent, tagged py3),
|
|
57
|
+
# and use manylinux_2_34 on Linux - its default toolchain is GCC 14, which the
|
|
58
|
+
# C++23 engine needs (the older manylinux images ship GCC 10-12).
|
|
59
|
+
[tool.cibuildwheel]
|
|
60
|
+
build = "cp312-*"
|
|
61
|
+
skip = "*musllinux*"
|
|
62
|
+
|
|
63
|
+
[tool.cibuildwheel.linux]
|
|
64
|
+
# Build x86_64 only: the 32-bit i686 build uses the legacy manylinux2014 image
|
|
65
|
+
# (GCC 10, no C++23), and nobody needs 32-bit x86 for this renderer.
|
|
66
|
+
archs = ["x86_64"]
|
|
67
|
+
manylinux-x86_64-image = "quay.io/pypa/manylinux_2_34_x86_64"
|
|
68
|
+
# manylinux_2_34 ships gcc-toolset-14 (GCC 14) but defaults to a legacy gcc; put
|
|
69
|
+
# the toolset first on PATH so gcc/g++ are C++23-capable.
|
|
70
|
+
environment = { PATH = "/opt/rh/gcc-toolset-14/root/usr/bin:$PATH", CC = "gcc", CXX = "g++" }
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
"""Python bindings for the RedNoise CPU software renderer.
|
|
2
|
+
|
|
3
|
+
This is a pure-Python binding built on :mod:`ctypes` over the stable C ABI
|
|
4
|
+
declared in ``include/rednoise/rednoise.h``. There is no compiled extension;
|
|
5
|
+
the binding loads the shared library (``librednoise.so`` on Linux,
|
|
6
|
+
``librednoise.dylib`` on macOS, ``rednoise.dll`` on Windows) at runtime.
|
|
7
|
+
|
|
8
|
+
The shared library is *not* bundled with this package. Build it from the C++
|
|
9
|
+
project::
|
|
10
|
+
|
|
11
|
+
cmake -B build -S . -DBUILD_LIB=ON -DBUILD_APP=OFF
|
|
12
|
+
cmake --build build
|
|
13
|
+
|
|
14
|
+
Then either place the library on a path the loader searches, or point the
|
|
15
|
+
``REDNOISE_LIBRARY`` environment variable at the built file.
|
|
16
|
+
|
|
17
|
+
Typical usage::
|
|
18
|
+
|
|
19
|
+
import rednoise
|
|
20
|
+
|
|
21
|
+
scene = rednoise.Scene.load_obj("assets/cornell-box.obj")
|
|
22
|
+
print(scene.triangle_count, rednoise.version())
|
|
23
|
+
image = scene.render(mode="pathtraced", width=400, height=300, samples=64)
|
|
24
|
+
image.save("cornell.png")
|
|
25
|
+
scene.close()
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import ctypes
|
|
31
|
+
import ctypes.util
|
|
32
|
+
import os
|
|
33
|
+
from enum import IntEnum
|
|
34
|
+
from typing import Optional, Tuple, Union
|
|
35
|
+
|
|
36
|
+
__all__ = ["RenderMode", "Scene", "Image", "version"]
|
|
37
|
+
|
|
38
|
+
__version__ = "0.1.2"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Render modes
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
class RenderMode(IntEnum):
|
|
45
|
+
"""Which renderer to run, mirroring the C ``rn_render_mode`` enum."""
|
|
46
|
+
|
|
47
|
+
WIREFRAME = 0
|
|
48
|
+
RASTERISED = 1
|
|
49
|
+
RAYTRACED = 2
|
|
50
|
+
PATHTRACED = 3
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def coerce(cls, value: Union["RenderMode", int, str]) -> "RenderMode":
|
|
54
|
+
"""Coerce an int, name string, or :class:`RenderMode` into a member.
|
|
55
|
+
|
|
56
|
+
Strings are matched case-insensitively, so ``"pathtraced"``,
|
|
57
|
+
``"PATHTRACED"`` and ``"path_traced"`` all resolve to
|
|
58
|
+
:attr:`RenderMode.PATHTRACED`.
|
|
59
|
+
"""
|
|
60
|
+
if isinstance(value, cls):
|
|
61
|
+
return value
|
|
62
|
+
if isinstance(value, bool):
|
|
63
|
+
# bool is a subclass of int; reject it to avoid surprises.
|
|
64
|
+
raise TypeError("render mode must not be a bool")
|
|
65
|
+
if isinstance(value, int):
|
|
66
|
+
return cls(value)
|
|
67
|
+
if isinstance(value, str):
|
|
68
|
+
key = value.strip().upper().replace("-", "").replace("_", "").replace(" ", "")
|
|
69
|
+
for member in cls:
|
|
70
|
+
if member.name.replace("_", "") == key:
|
|
71
|
+
return member
|
|
72
|
+
valid = ", ".join(m.name.lower() for m in cls)
|
|
73
|
+
raise ValueError(f"unknown render mode {value!r}; expected one of: {valid}")
|
|
74
|
+
raise TypeError(f"render mode must be RenderMode, int or str, not {type(value).__name__}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Library loading
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
def _candidate_paths() -> list:
|
|
81
|
+
"""Return candidate local build locations for the shared library."""
|
|
82
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
83
|
+
# Walk up from this package to plausible project roots.
|
|
84
|
+
roots = [
|
|
85
|
+
here, # next to the package itself
|
|
86
|
+
os.path.dirname(here), # bindings/python
|
|
87
|
+
os.path.dirname(os.path.dirname(here)), # bindings
|
|
88
|
+
os.path.dirname(os.path.dirname(os.path.dirname(here))), # project root
|
|
89
|
+
os.getcwd(), # wherever the process was launched from
|
|
90
|
+
]
|
|
91
|
+
names = [
|
|
92
|
+
"librednoise.so",
|
|
93
|
+
"librednoise.dylib",
|
|
94
|
+
"rednoise.dll",
|
|
95
|
+
"librednoise.dll",
|
|
96
|
+
]
|
|
97
|
+
subdirs = ["", "build", os.path.join("build", "Release"), os.path.join("build", "Debug")]
|
|
98
|
+
|
|
99
|
+
seen = set()
|
|
100
|
+
candidates = []
|
|
101
|
+
for root in roots:
|
|
102
|
+
for sub in subdirs:
|
|
103
|
+
for name in names:
|
|
104
|
+
path = os.path.normpath(os.path.join(root, sub, name))
|
|
105
|
+
if path not in seen:
|
|
106
|
+
seen.add(path)
|
|
107
|
+
candidates.append(path)
|
|
108
|
+
return candidates
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _bundled_library_path() -> Optional[str]:
|
|
112
|
+
"""Return the path to the library bundled inside this package, if present.
|
|
113
|
+
|
|
114
|
+
A pip-installed wheel ships the native library right next to this module
|
|
115
|
+
(installed there by the scikit-build-core / CMake build). Look for it first
|
|
116
|
+
so an installed package is self-contained and needs no environment setup.
|
|
117
|
+
"""
|
|
118
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
119
|
+
names = [
|
|
120
|
+
"librednoise.so",
|
|
121
|
+
"rednoise.dll",
|
|
122
|
+
"librednoise.dylib",
|
|
123
|
+
"librednoise.dll",
|
|
124
|
+
]
|
|
125
|
+
for name in names:
|
|
126
|
+
path = os.path.join(here, name)
|
|
127
|
+
if os.path.isfile(path):
|
|
128
|
+
return path
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _find_library_path() -> Optional[str]:
|
|
133
|
+
"""Locate the shared library path, or return ``None`` if not found."""
|
|
134
|
+
# 1. The library bundled next to this package (pip-installed wheel).
|
|
135
|
+
bundled = _bundled_library_path()
|
|
136
|
+
if bundled:
|
|
137
|
+
return bundled
|
|
138
|
+
|
|
139
|
+
# 2. Explicit override via environment variable.
|
|
140
|
+
env = os.environ.get("REDNOISE_LIBRARY")
|
|
141
|
+
if env:
|
|
142
|
+
if os.path.isfile(env):
|
|
143
|
+
return env
|
|
144
|
+
raise OSError(
|
|
145
|
+
f"REDNOISE_LIBRARY is set to {env!r} but that file does not exist."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# 3. System search path.
|
|
149
|
+
found = ctypes.util.find_library("rednoise")
|
|
150
|
+
if found:
|
|
151
|
+
return found
|
|
152
|
+
|
|
153
|
+
# 4. Common local build locations.
|
|
154
|
+
for path in _candidate_paths():
|
|
155
|
+
if os.path.isfile(path):
|
|
156
|
+
return path
|
|
157
|
+
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
_LIB = None # cached CDLL handle
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _load_library() -> ctypes.CDLL:
|
|
165
|
+
"""Load (once) and return the RedNoise shared library.
|
|
166
|
+
|
|
167
|
+
Loading is lazy so that ``import rednoise`` never fails merely because the
|
|
168
|
+
library has not been built yet. The first call that needs native code
|
|
169
|
+
triggers the load and raises a helpful error if the library is missing.
|
|
170
|
+
"""
|
|
171
|
+
global _LIB
|
|
172
|
+
if _LIB is not None:
|
|
173
|
+
return _LIB
|
|
174
|
+
|
|
175
|
+
path = _find_library_path()
|
|
176
|
+
if path is None:
|
|
177
|
+
raise OSError(
|
|
178
|
+
"Could not locate the RedNoise shared library "
|
|
179
|
+
"(librednoise.so / librednoise.dylib / rednoise.dll).\n"
|
|
180
|
+
"Build it with:\n"
|
|
181
|
+
" cmake -B build -S . -DBUILD_LIB=ON -DBUILD_APP=OFF\n"
|
|
182
|
+
" cmake --build build\n"
|
|
183
|
+
"then set the REDNOISE_LIBRARY environment variable to the built "
|
|
184
|
+
"library file, or place it on the loader search path."
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
lib = ctypes.CDLL(path)
|
|
189
|
+
except OSError as exc: # pragma: no cover - platform dependent
|
|
190
|
+
raise OSError(f"Failed to load RedNoise library at {path!r}: {exc}") from exc
|
|
191
|
+
|
|
192
|
+
_configure_signatures(lib)
|
|
193
|
+
_LIB = lib
|
|
194
|
+
return lib
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _configure_signatures(lib: ctypes.CDLL) -> None:
|
|
198
|
+
"""Declare argtypes/restype for every exported function."""
|
|
199
|
+
# rn_scene *rn_scene_load_obj(const char *path, float scale);
|
|
200
|
+
lib.rn_scene_load_obj.argtypes = [ctypes.c_char_p, ctypes.c_float]
|
|
201
|
+
lib.rn_scene_load_obj.restype = ctypes.c_void_p
|
|
202
|
+
|
|
203
|
+
# void rn_scene_free(rn_scene *scene);
|
|
204
|
+
lib.rn_scene_free.argtypes = [ctypes.c_void_p]
|
|
205
|
+
lib.rn_scene_free.restype = None
|
|
206
|
+
|
|
207
|
+
# int rn_scene_triangle_count(const rn_scene *scene);
|
|
208
|
+
lib.rn_scene_triangle_count.argtypes = [ctypes.c_void_p]
|
|
209
|
+
lib.rn_scene_triangle_count.restype = ctypes.c_int
|
|
210
|
+
|
|
211
|
+
# int rn_render(const rn_scene *, rn_render_mode, int, int, float, int, unsigned char *);
|
|
212
|
+
lib.rn_render.argtypes = [
|
|
213
|
+
ctypes.c_void_p,
|
|
214
|
+
ctypes.c_int,
|
|
215
|
+
ctypes.c_int,
|
|
216
|
+
ctypes.c_int,
|
|
217
|
+
ctypes.c_float,
|
|
218
|
+
ctypes.c_int,
|
|
219
|
+
ctypes.c_char_p,
|
|
220
|
+
]
|
|
221
|
+
lib.rn_render.restype = ctypes.c_int
|
|
222
|
+
|
|
223
|
+
# int rn_save_png(const char *path, int width, int height, const unsigned char *rgba);
|
|
224
|
+
lib.rn_save_png.argtypes = [
|
|
225
|
+
ctypes.c_char_p,
|
|
226
|
+
ctypes.c_int,
|
|
227
|
+
ctypes.c_int,
|
|
228
|
+
ctypes.c_char_p,
|
|
229
|
+
]
|
|
230
|
+
lib.rn_save_png.restype = ctypes.c_int
|
|
231
|
+
|
|
232
|
+
# const char *rn_version(void);
|
|
233
|
+
lib.rn_version.argtypes = []
|
|
234
|
+
lib.rn_version.restype = ctypes.c_char_p
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _encode_path(path: Union[str, os.PathLike]) -> bytes:
|
|
238
|
+
"""Encode a filesystem path for the C ABI (``const char *``)."""
|
|
239
|
+
return os.fsencode(os.fspath(path))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
# Image
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
class Image:
|
|
246
|
+
"""An RGBA8 image returned by :meth:`Scene.render`.
|
|
247
|
+
|
|
248
|
+
Holds the raw pixel bytes together with the image dimensions. Pixels are
|
|
249
|
+
laid out row-major as ``width * height`` RGBA quadruplets.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
__slots__ = ("_rgba", "_width", "_height")
|
|
253
|
+
|
|
254
|
+
def __init__(self, rgba: bytes, width: int, height: int):
|
|
255
|
+
expected = width * height * 4
|
|
256
|
+
if len(rgba) != expected:
|
|
257
|
+
raise ValueError(
|
|
258
|
+
f"rgba buffer has {len(rgba)} bytes, expected {expected} "
|
|
259
|
+
f"for a {width}x{height} RGBA8 image"
|
|
260
|
+
)
|
|
261
|
+
self._rgba = bytes(rgba)
|
|
262
|
+
self._width = int(width)
|
|
263
|
+
self._height = int(height)
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def rgba(self) -> bytes:
|
|
267
|
+
"""The raw RGBA8 pixel bytes (``width * height * 4`` bytes)."""
|
|
268
|
+
return self._rgba
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def width(self) -> int:
|
|
272
|
+
"""Image width in pixels."""
|
|
273
|
+
return self._width
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def height(self) -> int:
|
|
277
|
+
"""Image height in pixels."""
|
|
278
|
+
return self._height
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def size(self) -> Tuple[int, int]:
|
|
282
|
+
"""The ``(width, height)`` image dimensions."""
|
|
283
|
+
return (self._width, self._height)
|
|
284
|
+
|
|
285
|
+
def save(self, path: Union[str, os.PathLike]) -> None:
|
|
286
|
+
"""Write the image to ``path`` as a PNG via the native ``rn_save_png``.
|
|
287
|
+
|
|
288
|
+
Only the ``.png`` format is supported by the native writer. If Pillow
|
|
289
|
+
is installed and a non-PNG extension is requested, the save is delegated
|
|
290
|
+
to :meth:`to_pillow` instead.
|
|
291
|
+
"""
|
|
292
|
+
path = os.fspath(path)
|
|
293
|
+
ext = os.path.splitext(path)[1].lower()
|
|
294
|
+
if ext and ext != ".png":
|
|
295
|
+
# Fall back to Pillow for other formats if it is available.
|
|
296
|
+
try:
|
|
297
|
+
self.to_pillow().save(path)
|
|
298
|
+
return
|
|
299
|
+
except ImportError:
|
|
300
|
+
raise ValueError(
|
|
301
|
+
f"native saving only supports .png; got {ext!r}. "
|
|
302
|
+
"Install Pillow (pip install rednoise[pillow]) to save other formats."
|
|
303
|
+
)
|
|
304
|
+
lib = _load_library()
|
|
305
|
+
ok = lib.rn_save_png(_encode_path(path), self._width, self._height, self._rgba)
|
|
306
|
+
if ok != 1:
|
|
307
|
+
raise RuntimeError(f"rn_save_png failed to write {path!r}")
|
|
308
|
+
|
|
309
|
+
def to_pillow(self):
|
|
310
|
+
"""Build and return a :class:`PIL.Image.Image` (requires Pillow).
|
|
311
|
+
|
|
312
|
+
Pillow is imported lazily and is an optional dependency; install it via
|
|
313
|
+
``pip install rednoise[pillow]``.
|
|
314
|
+
"""
|
|
315
|
+
try:
|
|
316
|
+
from PIL import Image as PILImage
|
|
317
|
+
except ImportError as exc: # pragma: no cover - optional dependency
|
|
318
|
+
raise ImportError(
|
|
319
|
+
"Pillow is required for to_pillow(); install it with "
|
|
320
|
+
"'pip install rednoise[pillow]'"
|
|
321
|
+
) from exc
|
|
322
|
+
return PILImage.frombytes("RGBA", (self._width, self._height), self._rgba)
|
|
323
|
+
|
|
324
|
+
def __repr__(self) -> str:
|
|
325
|
+
return f"<rednoise.Image {self._width}x{self._height} RGBA8>"
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ---------------------------------------------------------------------------
|
|
329
|
+
# Scene
|
|
330
|
+
# ---------------------------------------------------------------------------
|
|
331
|
+
class Scene:
|
|
332
|
+
"""A loaded RedNoise scene backed by a native ``rn_scene`` handle.
|
|
333
|
+
|
|
334
|
+
Create one with :meth:`load_obj`. The underlying native scene is released
|
|
335
|
+
exactly once via :meth:`close`, ``__del__``, or context-manager exit.
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
__slots__ = ("_handle",)
|
|
339
|
+
|
|
340
|
+
def __init__(self, handle: int):
|
|
341
|
+
# handle is a non-NULL c_void_p value (an int address).
|
|
342
|
+
if not handle:
|
|
343
|
+
raise ValueError("Scene requires a non-NULL native handle")
|
|
344
|
+
self._handle = handle
|
|
345
|
+
|
|
346
|
+
@classmethod
|
|
347
|
+
def load_obj(cls, path: Union[str, os.PathLike], scale: float = 0.35) -> "Scene":
|
|
348
|
+
"""Load a Wavefront OBJ (and its .mtl) as a scene.
|
|
349
|
+
|
|
350
|
+
:param path: Path to the ``.obj`` file.
|
|
351
|
+
:param scale: Uniform scale applied to vertices (default ``0.35``).
|
|
352
|
+
:raises FileNotFoundError: if the OBJ file does not exist.
|
|
353
|
+
:raises RuntimeError: if the native loader returns NULL.
|
|
354
|
+
"""
|
|
355
|
+
path = os.fspath(path)
|
|
356
|
+
if not os.path.isfile(path):
|
|
357
|
+
raise FileNotFoundError(f"OBJ file not found: {path!r}")
|
|
358
|
+
lib = _load_library()
|
|
359
|
+
handle = lib.rn_scene_load_obj(_encode_path(path), ctypes.c_float(scale))
|
|
360
|
+
if not handle:
|
|
361
|
+
raise RuntimeError(f"rn_scene_load_obj failed to load {path!r}")
|
|
362
|
+
return cls(handle)
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def triangle_count(self) -> int:
|
|
366
|
+
"""Number of triangles in the scene."""
|
|
367
|
+
self._check_open()
|
|
368
|
+
lib = _load_library()
|
|
369
|
+
return int(lib.rn_scene_triangle_count(self._handle))
|
|
370
|
+
|
|
371
|
+
def render(
|
|
372
|
+
self,
|
|
373
|
+
mode: Union[RenderMode, int, str] = "raytraced",
|
|
374
|
+
width: int = 640,
|
|
375
|
+
height: int = 480,
|
|
376
|
+
cam_z: float = 4.0,
|
|
377
|
+
samples: int = 64,
|
|
378
|
+
) -> Image:
|
|
379
|
+
"""Render the scene and return an :class:`Image`.
|
|
380
|
+
|
|
381
|
+
:param mode: A :class:`RenderMode`, its integer value, or a name string
|
|
382
|
+
such as ``"wireframe"``, ``"rasterised"``, ``"raytraced"`` or
|
|
383
|
+
``"pathtraced"``.
|
|
384
|
+
:param width: Output width in pixels.
|
|
385
|
+
:param height: Output height in pixels.
|
|
386
|
+
:param cam_z: Camera Z position; the camera at ``(0, 0, cam_z)`` looks
|
|
387
|
+
at the origin.
|
|
388
|
+
:param samples: Per-pixel sample count (only used by ``PATHTRACED``).
|
|
389
|
+
:raises RuntimeError: if the native renderer reports failure.
|
|
390
|
+
"""
|
|
391
|
+
self._check_open()
|
|
392
|
+
render_mode = RenderMode.coerce(mode)
|
|
393
|
+
width = int(width)
|
|
394
|
+
height = int(height)
|
|
395
|
+
if width <= 0 or height <= 0:
|
|
396
|
+
raise ValueError(f"width and height must be positive, got {width}x{height}")
|
|
397
|
+
|
|
398
|
+
buf = ctypes.create_string_buffer(width * height * 4)
|
|
399
|
+
lib = _load_library()
|
|
400
|
+
ok = lib.rn_render(
|
|
401
|
+
self._handle,
|
|
402
|
+
int(render_mode),
|
|
403
|
+
width,
|
|
404
|
+
height,
|
|
405
|
+
ctypes.c_float(cam_z),
|
|
406
|
+
int(samples),
|
|
407
|
+
buf,
|
|
408
|
+
)
|
|
409
|
+
if ok != 1:
|
|
410
|
+
raise RuntimeError(
|
|
411
|
+
f"rn_render failed (mode={render_mode.name}, {width}x{height})"
|
|
412
|
+
)
|
|
413
|
+
return Image(buf.raw, width, height)
|
|
414
|
+
|
|
415
|
+
def close(self) -> None:
|
|
416
|
+
"""Release the native scene. Safe to call multiple times."""
|
|
417
|
+
handle = self._handle
|
|
418
|
+
if handle:
|
|
419
|
+
self._handle = 0
|
|
420
|
+
# Guard against interpreter-shutdown ordering issues.
|
|
421
|
+
if _LIB is not None:
|
|
422
|
+
_LIB.rn_scene_free(handle)
|
|
423
|
+
|
|
424
|
+
def _check_open(self) -> None:
|
|
425
|
+
if not self._handle:
|
|
426
|
+
raise RuntimeError("operation on a closed Scene")
|
|
427
|
+
|
|
428
|
+
def __enter__(self) -> "Scene":
|
|
429
|
+
return self
|
|
430
|
+
|
|
431
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
432
|
+
self.close()
|
|
433
|
+
|
|
434
|
+
def __del__(self):
|
|
435
|
+
# Best-effort cleanup; swallow everything during interpreter shutdown.
|
|
436
|
+
try:
|
|
437
|
+
self.close()
|
|
438
|
+
except Exception:
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
def __repr__(self) -> str:
|
|
442
|
+
if not self._handle:
|
|
443
|
+
return "<rednoise.Scene closed>"
|
|
444
|
+
return f"<rednoise.Scene handle=0x{self._handle:x}>"
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# ---------------------------------------------------------------------------
|
|
448
|
+
# Module-level helpers
|
|
449
|
+
# ---------------------------------------------------------------------------
|
|
450
|
+
def version() -> str:
|
|
451
|
+
"""Return the native library version string, e.g. ``"0.1.0"``."""
|
|
452
|
+
lib = _load_library()
|
|
453
|
+
raw = lib.rn_version()
|
|
454
|
+
if raw is None:
|
|
455
|
+
return ""
|
|
456
|
+
return raw.decode("utf-8", "replace")
|