====== Fast Noise generation in python with FastNoise2 ======
{{tag>dev python cpp noise nervproj fastnoise2}}
So, today, we will start a somewhat larger project, to try to provide some efficient/fast noise generation support in python using the FastNoise2 library. I know we already have bindings available for the [[https://pyfastnoisesimd.readthedocs.io/en/latest/overview.html|previous version FastNoiseSIMD]], but still I want to try building the required elements myself for once and see how far I can go with this ๐
So, let's rock it baby!
====== ======
===== Target GUI implementation =====
=> Ideally I would like to be able to build a GUI like what we have on this page https://github.com/Auburn/FastNoiseSIMD using those new bindings and pyQT5 ๐!
{{ blog:2022:0430:fastnoise_gui.png?800 }}
===== First step: Building the FastNoise2 C++ library =====
* Let's prepare a builder for that library in NervProj:
* **Note**: I cloned the FastNoise2 repository on github of course: https://github.com/roche-emmanuel/FastNoise2
* I start with creating a minimal/empty builder class: """This module provide the builder for the FastNoise2 library."""
import logging
from nvp.components.build import BuildManager
from nvp.nvp_builder import NVPBuilder
logger = logging.getLogger(__name__)
def register_builder(bman: BuildManager):
"""Register the build function"""
bman.register_builder('FastNoise2', FastNoise2Builder(bman))
class FastNoise2Builder(NVPBuilder):
"""FastNoise2 builder class."""
def build_on_windows(self, build_dir, prefix, _desc):
"""Build method for FastNoise2 on windows"""
def build_on_linux(self, build_dir, prefix, desc):
"""Build method for FastNoise2 on linux"""
* Then let's get a preview of the source structure: $nvp build libs fastnoise2 --preview
* OK: so using Cmake, and to build the **NoiseTool** gui we would need some additional dependencies like "Magnum", "imgui" and "glfw" from what I see => let's keep this out for the moment (or maybe it is integrated automatically during the build since the cmake files are referencing github repositories ? Could be worth to try it)
* Alos taking into account the instructions from this page: https://github.com/Auburn/FastNoise2/wiki/1:-Compiling-FastNoise2
* So first trial with the following builder content: class FastNoise2Builder(NVPBuilder):
"""FastNoise2 builder class."""
def build_on_windows(self, build_dir, prefix, _desc):
"""Build method for FastNoise2 on windows"""
flags = ["-S", ".", "-B", "build"]
self.run_cmake(build_dir, prefix, flags=flags)
self.run_ninja(build_dir)
def build_on_linux(self, build_dir, prefix, desc):
"""Build method for FastNoise2 on linux"""
flags = ["-S", ".", "-B", "build"]
self.run_cmake(build_dir, prefix, flags=flags)
self.run_ninja(build_dir)
* Stating the build: $ nvp build libs fastnoise2 -k
* And we get the error message: -- Detecting CXX compile features - done
-- CPM: adding package corrade@0 (a8065db3c55aec214aa4e4887c4073289b2988e2)
CMake Error at D:/Projects/NervProj/tools/windows/cmake-3.22.3/share/cmake-3.22/Modules/ExternalProject.cmake:2666 (message):
error: could not find git for clone of corrade-populate
Call Stack (most recent call first):
D:/Projects/NervProj/tools/windows/cmake-3.22.3/share/cmake-3.22/Modules/ExternalProject.cmake:3716 (_ep_add_download_command)
CMakeLists.txt:23 (ExternalProject_Add)
* => That sounds legitimate: we need git to be available to download the additional projects, so let's add it to the PATH (and fix the ninja build dir at the same time ;-): def build_on_windows(self, build_dir, prefix, _desc):
"""Build method for FastNoise2 on windows"""
base_dir = self.tools.get_tool_dir('git')
logger.info("Using git dir: %s", base_dir)
pdirs = self.env.get("PATH", "")
self.env['PATH'] = f"{base_dir};{pdirs}"
flags = ["-S", ".", "-B", "build"]
self.run_cmake(build_dir, prefix, flags=flags)
sub_dir = self.get_path(build_dir, "build")
self.run_ninja(sub_dir)
* But naaay.... still not quite working with an error from "Corrade" from what I see: -- Configuring done
-- Generating done
-- Build files have been written to: D:/Projects/NervProj/libraries/build/FastNoise2.git/build
[17/156] Building CXX object _deps\corrade-build\src\Corrade\Utility\CMakeFiles\CorradeUtility.dir\Format.cpp.obj
FAILED: _deps/corrade-build/src/Corrade/Utility/CMakeFiles/CorradeUtility.dir/Format.cpp.obj
D:\Softs\VisualStudio2022CE\VC\Tools\MSVC\14.31.31103\bin\Hostx64\x64\cl.exe /nologo /TP -DNOMINMAX -DUNICODE -DWIN32_LEAN_AND_MEAN -D_CRT_SECURE_NO_WAR
NINGS -D_SCL_SECURE_NO_WARNINGS -D_UNICODE -ID:\Projects\NervProj\libraries\build\FastNoise2.git\build\_deps\corrade-src\src -ID:\Projects\NervProj\libra
ries\build\FastNoise2.git\build\_deps\corrade-build\src /DWIN32 /D_WINDOWS /GR /EHsc /MD /O2 /Ob2 /DNDEBUG /FS /W4 /wd4251 /wd4244 /wd4267 /wd4351 /wd43
73 /wd4510 /wd4610 /wd4512 /wd4661 /wd4702 /wd4706 /wd4800 /wd4910 -std:c++17 /showIncludes /Fo_deps\corrade-build\src\Corrade\Utility\CMakeFiles\Corrade
Utility.dir\Format.cpp.obj /Fdpdb-files\ /FS -c D:\Projects\NervProj\libraries\build\FastNoise2.git\build\_deps\corrade-src\src\Corrade\Utility\Format.cp
p
D:\Projects\NervProj\libraries\build\FastNoise2.git\build\_deps\corrade-src\src\Corrade\Utility\Format.cpp(402): error C2666: '+'ย : les 2 surcharges ont
des conversions similaires
D:\Projects\NervProj\libraries\build\FastNoise2.git\build\_deps\corrade-src\src\Corrade\Utility\Format.cpp(402): note: est peut-รชtre 'built-in C++ operat
or+(bool, size_t)'
D:\Projects\NervProj\libraries\build\FastNoise2.git\build\_deps\corrade-src\src\Corrade\Utility\Format.cpp(402): note: ou 'built-in C++ operator+(T
, __int64)'
with
[
T=char
]
D:\Projects\NervProj\libraries\build\FastNoise2.git\build\_deps\corrade-src\src\Corrade\Utility\Format.cpp(402): note: lors de la tentative de mise en co
rrespondance de la liste des arguments '(const Corrade::Containers::ArrayView, size_t)'
[23/156] Building CXX object _deps\corrade-build\src\Corrade\Utility\CMakeFiles\CorradeUtilityObjects.dir\Debug.cpp.obj
D:\Projects\NervProj\libraries\build\FastNoise2.git\build\_deps\corrade-src\src\Corrade\Utility\Debug.cpp(468) : warning C4722: 'Corrade::Utility::Fatal:
:~Fatal'ย : aucun retour du destructeur, fuite de mรฉmoire possible
[26/156] Building CXX object _deps\corrade-build\src\Corrade\Utility\CMakeFiles\CorradeUtility.dir\Resource.cpp.obj
ninja: build stopped: subcommand failed.
Traceback (most recent call last):
File "D:\Projects\NervProj\cli.py", line 5, in
ctx.run()
File "D:\Projects\NervProj\nvp\nvp_context.py", line 291, in run
if comp.process_command(cmd):
File "D:\Projects\NervProj\nvp\components\build.py", line 385, in process_command
self.check_libraries(dlist)
File "D:\Projects\NervProj\nvp\components\build.py", line 237, in check_libraries
self.deploy_dependency(dep)
File "D:\Projects\NervProj\nvp\components\build.py", line 290, in deploy_dependency
builder.build(build_dir, prefix, desc)
File "D:\Projects\NervProj\nvp\nvp_builder.py", line 33, in build
self.build_on_windows(build_dir, prefix, desc)
File "D:\Projects\NervProj\nvp\builders\fastnoise2.py", line 31, in build_on_windows
self.run_ninja(sub_dir)
File "D:\Projects\NervProj\nvp\nvp_builder.py", line 84, in run_ninja
self.exec_ninja(build_dir)
File "D:\Projects\NervProj\nvp\nvp_builder.py", line 80, in exec_ninja
self.execute([ninja_path]+flags, cwd=build_dir, env=self.env)
File "D:\Projects\NervProj\nvp\nvp_object.py", line 383, in execute
subprocess.check_call(cmd, stdout=stdout, stderr=stderr, cwd=cwd, env=env)
File "D:\Projects\NervProj\tools\windows\python-3.10.1\lib\subprocess.py", line 369, in check_call
raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command '['D:\\Projects\\NervProj\\tools\\windows\\ninja-1.10.2\\ninja.exe']' returned non-zero exit status 1.
/*__*/
* => Never mind, I don't really need to build the "NoiseTool" myself for now, so let's try to bypass that part: def build_on_windows(self, build_dir, prefix, _desc):
"""Build method for FastNoise2 on windows"""
# base_dir = self.tools.get_tool_dir('git')
# logger.info("Using git dir: %s", base_dir)
# pdirs = self.env.get("PATH", "")
# self.env['PATH'] = f"{base_dir};{pdirs}"
flags = ["-S", ".", "-B", "build", "-DFASTNOISE2_NOISETOOL=OFF"]
self.run_cmake(build_dir, prefix, flags=flags)
sub_dir = self.get_path(build_dir, "build")
self.run_ninja(sub_dir)
* And that was way faster/better ๐ ending with: -- Installing: D:/Projects/NervProj/libraries/windows_msvc/FastNoise2-0.9.4/include/FastNoise/Generators/Perlin.h
-- Installing: D:/Projects/NervProj/libraries/windows_msvc/FastNoise2-0.9.4/include/FastNoise/Generators/Simplex.h
-- Installing: D:/Projects/NervProj/libraries/windows_msvc/FastNoise2-0.9.4/include/FastNoise/Generators/Value.h
-- Installing: D:/Projects/NervProj/libraries/windows_msvc/FastNoise2-0.9.4/lib/cmake/FastNoise2/FastNoise2Config.cmake
-- Installing: D:/Projects/NervProj/libraries/windows_msvc/FastNoise2-0.9.4/lib/cmake/FastNoise2/FastNoise2ConfigVersion.cmake
-- Installing: D:/Projects/NervProj/libraries/windows_msvc/FastNoise2-0.9.4/lib/cmake/FastNoise2/FastNoise2Targets.cmake
-- Installing: D:/Projects/NervProj/libraries/windows_msvc/FastNoise2-0.9.4/lib/cmake/FastNoise2/FastNoise2Targets-release.cmake
-- Up-to-date: D:/Projects/NervProj/libraries/windows_msvc/FastNoise2-0.9.4/lib
2022/04/30 08:54:14 [nvp.components.build] INFO: Removing build folder D:\Projects\NervProj\libraries\build\FastNoise2.git
2022/04/30 08:54:14 [nvp.components.build] INFO: Done building FastNoise2-0.9.4 (build time: 14.35 seconds)
2022/04/30 08:54:14 [nvp.components.build] INFO: All libraries OK.
* => So now I have the **FastNoise2-0.9.4** package built for windows. Next, let's try the build on linux.
* **OK**: No problem on linux either: -- Installing: /mnt/data1/dev/projects/NervProj/libraries/linux_clang/FastNoise2-0.9.4/include/FastNoise/Generators/Modifiers.h
-- Installing: /mnt/data1/dev/projects/NervProj/libraries/linux_clang/FastNoise2-0.9.4/include/FastNoise/Generators/Perlin.h
-- Installing: /mnt/data1/dev/projects/NervProj/libraries/linux_clang/FastNoise2-0.9.4/include/FastNoise/Generators/Simplex.h
-- Installing: /mnt/data1/dev/projects/NervProj/libraries/linux_clang/FastNoise2-0.9.4/include/FastNoise/Generators/Value.h
-- Installing: /mnt/data1/dev/projects/NervProj/libraries/linux_clang/FastNoise2-0.9.4/lib/cmake/FastNoise2/FastNoise2Config.cmake
-- Installing: /mnt/data1/dev/projects/NervProj/libraries/linux_clang/FastNoise2-0.9.4/lib/cmake/FastNoise2/FastNoise2ConfigVersion.cmake
-- Installing: /mnt/data1/dev/projects/NervProj/libraries/linux_clang/FastNoise2-0.9.4/lib/cmake/FastNoise2/FastNoise2Targets.cmake
-- Installing: /mnt/data1/dev/projects/NervProj/libraries/linux_clang/FastNoise2-0.9.4/lib/cmake/FastNoise2/FastNoise2Targets-release.cmake
2022/04/30 09:57:53 [nvp.components.build] INFO: Removing build folder /mnt/data1/dev/projects/NervProj/libraries/build/FastNoise2.git
2022/04/30 09:57:53 [nvp.components.build] INFO: Done building FastNoise2-0.9.4 (build time: 6.01 seconds)
2022/04/30 09:57:53 [nvp.components.build] INFO: All libraries OK.
kenshin@neptune:/mnt/data1/dev/projects/NervProj$
* So FastNoise2 library built just fine! Now let's look at the bindings generation for python ๐คช
===== Second step: Adding pythin bindings support in boost libraries =====
* Reading this article: https://realpython.com/python-bindings-overview/
* So all the options described in the article above seem interesting to generate python bindings, yet to get the highest level of flexibility I would be tempted to select **pybind11** and to push it even further, I'm thinking that maybe using boost.python directly would be the best option in my case, because this is something I have done already in the past, but this also means I need to rebuild boost with python as a dependency this time. Let's see if I can do that properly...
* **OK** So the update the boost builder was rather simple, and now I could rebuild the library package with python support on both windows and linux simply adding one line in the ''user-config.jam'' file below: class BoostBuilder(NVPBuilder):
"""Boost builder class."""
def build_on_windows(self, build_dir, prefix, desc):
"""Build the boost library on windows"""
# Note: we always have to use the msvc compiler to do the bootstrap:
msvc_comp = self.man.get_compiler('msvc')
msvc_env = msvc_comp.get_env()
logger.info("Building boost library...")
bs_cmd = ['bootstrap.bat', '--without-icu']
bs_cmd = ['cmd.exe', '/c', " ".join(bs_cmd)]
logger.info("Executing bootstrap command: %s", bs_cmd)
self.execute(bs_cmd, cwd=build_dir, env=msvc_env)
if self.compiler.is_clang():
self.build_with_clang(build_dir, prefix)
else:
# Build with MSVC compiler:
assert self.compiler.is_msvc(), "Expected MSVC compiler here."
# logger.info("Using build env: %s", self.pretty_print(msvc_env))
py_path = self.tools.get_tool_path("python").replace("\\", "/")
py_vers = self.tools.get_tool_desc("python")["version"].split(".")
with open(self.get_path(build_dir, "user-config.jam"), "w", encoding="utf-8") as file:
# Add the entry for python:
file.write(f"using python : {py_vers[0]}.{py_vers[1]} : {py_path} ;\n")
# Note: updated below to use runtime-link=shared instead of runtime-link=static
bjam_cmd = [build_dir + '/b2.exe', "--user-config=user-config.jam", "--prefix=" + prefix,
"--without-mpi", "-sNO_BZIP2=1", "toolset=msvc", "architecture=x86",
"address-model=64", "variant=release", "link=static", "threading=multi",
"runtime-link=shared", "install"]
logger.info("Executing bjam command: %s", bjam_cmd)
self.execute(bjam_cmd, cwd=build_dir, env=msvc_env)
# Next, in both cases we need some cleaning in the installed boost folder, fixing the include path:
# include/boost-1_78/boost -> include/boost
vers = desc['version'].split('.')
bfolder = f"boost-{vers[0]}_{vers[1]}"
src_inc_dir = self.get_path(prefix, "include", bfolder, "boost")
dst_inc_dir = self.get_path(prefix, "include", "boost")
self.move_path(src_inc_dir, dst_inc_dir)
self.remove_folder(self.get_path(prefix, "include", bfolder))
def build_on_linux(self, build_dir, prefix, _desc):
"""Build the boost library on linux"""
# compiler should be clang for now:
assert self.compiler.is_clang(), "Only clang is supported on linux to build boost."
self.build_with_clang(build_dir, prefix)
def build_with_clang(self, build_dir, prefix):
"""Build with the clang compiler"""
logger.info("Building boost library...")
build_env = self.compiler.get_env()
# logger.info("Using build env: %s", self.pretty_print(build_env))
comp_path = self.compiler.get_cxx_path()
cxxflags = self.compiler.get_cxxflags()
linkflags = self.compiler.get_linkflags()
ext = ".exe" if self.is_windows else ""
if self.is_linux:
# Note: the bootstrap.sh script above is crap, so instead we build b2 manually ourself here:
script_file = self.get_path(build_dir, f"./tools/build/src/engine/build.sh")
bs_cmd = [script_file, "clang", f"--cxx={comp_path}", f"--cxxflags={cxxflags}"]
logger.info("Building B2 command: %s", bs_cmd)
self.execute(bs_cmd, cwd=build_dir)
bjam_file = self.get_path(build_dir, f"b2{ext}")
self.copy_file(self.get_path(build_dir, f"tools/build/src/engine/b2{ext}"), bjam_file)
self.add_execute_permission(bjam_file)
# for windows:
# cf. https://gist.github.com/oxycoder/98864df68f7a879066c51c181a492fe2
# Ensure we use backslashes:
comp_dir = self.compiler.get_cxx_dir().replace("\\", "/")
comp_path = comp_path.replace("\\", "/")
py_path = self.tools.get_tool_path("python").replace("\\", "/")
py_vers = self.tools.get_tool_desc("python")["version"].split(".")
with open(self.get_path(build_dir, "user-config.jam"), "w", encoding="utf-8") as file:
# Note: Should not add the -std=c++11 flag below as this will lead to an error with C files:
file.write(f"using clang : : {comp_path} : ")
if self.is_windows:
file.write("cxxstd=17 ")
file.write(f"\"{comp_dir}/llvm-ranlib.exe\" ")
file.write(f"\"{comp_dir}/llvm-ar.exe\" ")
file.write("-D_CRT_SECURE_NO_WARNINGS ")
# file.write(f"-D_SILENCE_CXX17_OLD_ALLOCATOR_MEMBERS_DEPRECATION_WARNING ")
file.write(";\n")
else:
file.write(f"\"{cxxflags} -fPIC\" ")
file.write(f"\"{linkflags}\" ;\n")
# Add the entry for python:
file.write(f"using python : {py_vers[0]}.{py_vers[1]} : {py_path} ;\n")
# "--with-python="+pyPath+"/bin/python3", "--with-python-root="+pyPath
# Note: below we need to run bjam with links to the clang libraries:
bjam = self.get_path(build_dir, f'./b2{ext}')
# tgt_os = "windows" if self.is_windows else "linux"
# f"target-os={tgt_os}",
bjam_cmd = [bjam, "--user-config=user-config.jam",
"--buildid=clang", "-j", "8", "toolset=clang",
"--prefix="+prefix, "--without-mpi", "-sNO_BZIP2=1",
"architecture=x86", "variant=release", "link=static", "threading=multi",
"address-model=64"]
if self.is_windows:
bjam_cmd.append("runtime-link=shared")
bjam_cmd.append("install")
logger.info("Executing bjam command: %s", bjam_cmd)
self.execute(bjam_cmd, cwd=build_dir, env=build_env)
===== Deploying my own C++ projects =====
* Next I have to think a little... because the goal here is to write a C++ project, that will generate a python module containing the bindings I want for the FastNoise2 library.
* And I want some other python project that I'm building to be able to install/import that binding module.
* So how should I handle that ?
* I'm thinking I could describe a list of "cmake projects" that might be loaded from inside the parent NervProj project of other sub project.
* And then I could trigger the installation of such projects into a given folder.
* So other sub projects might then request the installation of those cmake projects into a given local folder...
* Sounds a bit tricky, but I cannot think of anything else for the moment.
* So let's prepare something in this direction...
* => **OK**, so now I have a first version of a **"Blueprint manager"** component in **NervProj**: """BlueprintManager module"""
import logging
from nvp.nvp_component import NVPComponent
from nvp.nvp_context import NVPContext
from nvp.nvp_project import NVPProject
from nvp.nvp_builder import NVPBuilder
logger = logging.getLogger(__name__)
def register_component(ctx: NVPContext):
"""Register this component in the given context"""
comp = BlueprintManager(ctx)
ctx.register_component('blueprint', comp)
class BlueprintManager(NVPComponent):
"""Project command manager class"""
def __init__(self, ctx: NVPContext):
"""Project commands manager constructor"""
NVPComponent.__init__(self, ctx)
desc = {
"bprint": {"build": None}
}
self.blueprints = None
self.builder = None
self.build_dir = None
self.default_install_dir = None
ctx.define_subparsers("main", desc)
psr = ctx.get_parser('main.bprint.build')
psr.add_argument("bp_names", type=str,
help="List of blueprints that we should build")
psr.add_argument("-d", "--dir", dest='bp_install_dir', type=str,
help="Destination where to install the blue prints")
def process_command(self, cmd0):
"""Re-implementation of the process_command method."""
if cmd0 == 'bprint':
cmd1 = self.ctx.get_command(1)
if cmd1 == 'build':
bprints = self.get_param("bp_names").split(",")
dest_dir = self.get_param("bp_install_dir", None)
self.install_blueprints(bprints, dest_dir)
return True
return False
def initialize(self):
"""Initialize this component as needed before usage."""
if self.initialized is False:
self.build_dir = self.get_path(self.ctx.get_root_dir(), "build")
self.default_install_dir = self.get_path(self.ctx.get_root_dir(), "dist", "bin")
self.collect_blueprints()
bman = self.get_component('builder')
self.builder = NVPBuilder(bman)
self.builder.init_env()
self.initialized = True
def collect_blueprints(self):
"""Collect the available blueprints"""
if self.blueprints is None:
self.blueprints = self.config.get("blueprints", {})
root_dir = self.ctx.get_root_dir()
for _name, desc in self.blueprints.items():
desc['url'] = desc['url'].replace("${NVP_ROOT_DIR}", root_dir)
return self.blueprints
def install_blueprints(self, bp_names, install_dir):
"""Install the list of blueprints"""
self.initialize()
blueprints = self.blueprints
# Iterate on all the blueprint names:
for bp_name in bp_names:
assert bp_name in blueprints, f"Cannot find blueprint {bp_name}"
self.install_blueprint(bp_name, install_dir)
def install_blueprint(self, bp_name, install_dir):
"""Install a specific blueprint"""
if install_dir is None:
install_dir = self.default_install_dir
desc = self.blueprints[bp_name]
# we should run a cmake command
build_dir = self.get_path(self.build_dir, bp_name)
self.make_folder(build_dir)
src_dir = desc["url"]
flags = []
# check if we have dependencies:
deps = desc.get("dependencies", {})
bman = self.get_component('builder')
tool = self.get_component('tools')
for var_name, tgt in deps.items():
# For now we just expect the target to be a library name:
# or a tool name:
parts = tgt.split(":")
lib_name = parts[0]
vtype = "root_dir" if len(parts) == 1 else parts[1]
if vtype == "root_dir":
if bman.has_library(lib_name):
var_val = bman.get_library_root_dir(lib_name)
else:
var_val = tool.get_tool_root_dir(lib_name)
var_val = var_val.replace("\\", "/")
elif vtype == "version_major":
desc = bman.get_library_desc(lib_name) or tool.get_tool_desc(lib_name)
parts = desc['version'].split(".")
var_val = parts[0]
elif vtype == "version_minor":
desc = bman.get_library_desc(lib_name) or tool.get_tool_desc(lib_name)
parts = desc['version'].split(".")
var_val = parts[1]
flags.append(f"-D{var_name}={var_val}")
self.builder.run_cmake(build_dir, install_dir, src_dir, flags)
self.builder.run_ninja(build_dir)
* That component can use the description of some "blueprints" in the nervproj config file (to be extended later to also search in sub project configs) defined as follow: // List of local blueprints that we can build:
"blueprints": {
// Fastnoise2 python bindings:
"pyfn2": {
"url": "${NVP_ROOT_DIR}/sources/pyfn2",
"dependencies": {
"BOOST_DIR": "boost",
"PYTHON_DIR": "python",
"PY_VERS_MAJOR": "python:version_major",
"PY_VERS_MINOR": "python:version_minor"
}
}
}
* And with that we should be able to build and then install some of our own elements from sources ;-).
Thinking more about it I eventually decided to switch the name from "blueprint" to "module" to handle this concept here. So the **BlueprintManager** I describe above was eventually replaced with a **ModuleManager**, and the config entries were updated accordingly of course.
===== Setting up the context to install our modules =====
* **OK**: So I updated the module manager component adding support for an "install" command, that will install a full **"module set"** defined for instance as follow in a sub project config: // Required modules sets in this project:
"module_sets": {
"default": [
{
// fastnoise2 python bindings
"name": "pyfn2",
"dir": "${PROJECT_ROOT_DIR}/extensions/"
}
]
}
* With this we can trigger the install of the required modules using the command: $ nvp -p t7 mods install
2022/05/01 15:29:16 [nvp.nvp_compiler] INFO: MSVC root dir is: D:\Softs\VisualStudio2022CE
2022/05/01 15:29:16 [nvp.nvp_compiler] INFO: Found msvc-14.31.31103
2022/05/01 15:29:16 [nvp.components.build] INFO: Selecting compiler msvc-14.31.31103
2022/05/01 15:29:16 [nvp.nvp_compiler] INFO: Initializing MSVC compiler environment...
2022/05/01 15:29:18 [nvp.components.module_manager] INFO: Should install module set 'default' in resp_t7
2022/05/01 15:29:18 [nvp.components.module_manager] INFO: Should install module pyfn2
2022/05/01 15:29:18 [nvp.nvp_builder] INFO: Cmake command: ['D:\\Projects\\NervProj\\tools\\windows\\cmake-3.22.3\\bin\\cmake.exe', '-G', 'Ninja', '-D
CMAKE_BUILD_TYPE=Release', '-DCMAKE_INSTALL_PREFIX=D:\\Projects\\resp_t7/extensions/', '-DBOOST_DIR=D:/Projects/NervProj/libraries/windows_msvc/boost-
1.78.0', '-DPYTHON_DIR=D:/Projects/NervProj/tools/windows/python-3.10.1', '-DPY_VERS_MAJOR=3', '-DPY_VERS_MINOR=10', 'D:\\Projects\\NervProj/sources/p
yfn2']
-- Configuring done
-- Generating done
-- Build files have been written to: D:/Projects/NervProj/build/pyfn2
[2/2] Linking CXX shared library pyfn2.pyd
[1/2] Install the project...
-- Install configuration: "Release"
-- Installing: D:/Projects/resp_t7/extensions/./pyfn2.pyd
* => Great ๐!
===== Adding some initial meat to the pyfn2 binding module =====
* Now let's add some initial bindings in that **pyfn2** C++ module
* Found this page: https://wiki.python.org/moin/boost.python/HowTo
* => I updated the **scripts** runner component to support scripts in **NervProj** and sub project.
* I then added the pytest script below to test the pyfn2 bindings: "scripts": {
"test-pyfn2": {
"cmd": "${PYTHON} -m pytest -s",
"cwd": "${NVP_ROOT_DIR}/tests/pyfn2",
"python_path": ["${NVP_ROOT_DIR}"]
}
}
* And now I can test changes in the module with the calls: $ nvp mods build pyfn2
$ nvp run test-pyfn2
* I built a very minimal pytest file: """Test module for pyfn2 module"""
# import sys
# print(sys.path)
class TestBindings():
"""Test class pyfn2 bindings"""
def test_sanity(self):
"""Sanity check"""
assert 1 == 1
def test_import(self):
"""Test import of pyfn2 module"""
from dist.bin import pyfn2
pyfn2.hello()
* And this works as good already โ: $ nvp run test-pyfn2
2022/05/01 17:04:02 [nvp.components.runner] INFO: Using python path: D:/Projects/NervProj
================================================================ test session starts ================================================================
platform win32 -- Python 3.10.1, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\Projects\NervProj\tests\pyfn2
collected 2 items
test_pyfn2.py .Hello from pyfn2 binding module!
.
================================================================= 2 passed in 0.02s =================================================================
* Now let's add the actual classes in the bindings...
* **Note** Found this article on numpy arrays usage in boost python: https://cosmiccoding.com.au/tutorials/boost
* In the process noticed an error when trying to use numpy in the bindings: LINK: command "D:\Softs\VisualStudio2022CE\VC\Tools\MSVC\14.31.31103\bin\Hostx64\x64\link.exe /nologo CMakeFiles\pyfn2.dir\bindings.cpp.obj /out:pyfn2
.pyd /implib:pyfn2.lib /pdb:pyfn2.pdb /dll /version:0.0 /machine:x64 /INCREMENTAL:NO -LIBPATH:D:\Projects\NervProj\libraries\windows_msvc\boost-1.78.0
\lib -LIBPATH:D:\Projects\NervProj\libraries\windows_msvc\FastNoise2-0.9.4\lib -LIBPATH:D:\Projects\NervProj\tools\windows\python-3.10.1\libs FastNois
e.lib kernel32.lib user32.lib gdi32.lib winspool.lib shell32.lib ole32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.lib /MANIFEST /MANIFESTFILE:pyf
n2.pyd.manifest failed (exit code 1104) with the following output:
LINK : fatal error LNK1104: impossible d'ouvrir le fichier 'boost_numpy310-vc143-mt-x64-1_78.lib'
ninja: build stopped: subcommand failed.
Traceback (most recent call last):
File "D:\Projects\NervProj\cli.py", line 5, in
ctx.run()
File "D:\Projects\NervProj\nvp\nvp_context.py", line 315, in run
if comp.process_command(cmd):
File "D:\Projects\NervProj\nvp\components\module_manager.py", line 56, in process_command
self.build_modules(bprints, dest_dir)
File "D:\Projects\NervProj\nvp\components\module_manager.py", line 98, in build_modules
self.build_module(bp_name, install_dir)
File "D:\Projects\NervProj\nvp\components\module_manager.py", line 147, in build_module
self.builder.run_ninja(build_dir)
File "D:\Projects\NervProj\nvp\nvp_builder.py", line 90, in run_ninja
self.exec_ninja(build_dir)
File "D:\Projects\NervProj\nvp\nvp_builder.py", line 86, in exec_ninja
self.execute([ninja_path]+flags, cwd=build_dir, env=self.env)
File "D:\Projects\NervProj\nvp\nvp_object.py", line 383, in execute
subprocess.check_call(cmd, stdout=stdout, stderr=stderr, cwd=cwd, env=env)
File "D:\Projects\NervProj\tools\windows\python-3.10.1\lib\subprocess.py", line 369, in check_call
raise CalledProcessError(retcode, cmd)
* Then I found that page on the numpy extension build process: https://www.badprog.com/c-boost-building-the-boost-python-numpy-extension-as-a-library
* => But actually the numpy support is built already, my only issue is that I need to notify the build system that I want to use the static version.
* To read also: https://vsamy.github.io/fr/blog/boost-python-cmake-build
* **Solution**: Also add to call **np::initialize()** at the beginning of the boost binding module: BOOST_PYTHON_MODULE(pyfn2)
{
Py_Initialize();
np::initialize();
// Bindings here
}
===== First working version =====
* And so, after some good work on the bindings, and unit test generation, I now have an initial version covering some of the most common nodes in FastNoise2 as defined in the bindings file below.
* **Note**: I first worked on windows with the MSVC compiler, then I had to change a bit the includes/initial structure of the template glue code at the beginning of the bindings file to make clang happy on linux, but nothing too serious anyway: #include
static void hello()
{
std::cout << "Hello from pyfn2 binding module!"<< std::endl;
}
// cf. https://stackoverflow.com/questions/14355441/using-custom-smart-pointers-in-boost-python
// cf. http://pyplusplus.readthedocs.io/en/latest/troubleshooting_guide/smart_ptrs/bindings.cpp.html
// cf. http://boost.org/libs/python/doc/v2/register_ptr_to_python.html
// cf. https://stackoverflow.com/questions/18720165/smart-pointer-casting-in-boostpython
namespace FastNoise {
template
class SmartNode;
}
// // here comes the magic
// template T* get_pointer(FastNoise::SmartNode const& p) {
// //notice the const_cast<> at this point
// //for some unknown reason, bp likes to have it like that
// return const_cast(p.get());
// }
// some boost.python plumbing is required as you already know
namespace boost {
namespace python {
// here comes the magic
template T* get_pointer(FastNoise::SmartNode const& p) {
//notice the const_cast<> at this point
//for some unknown reason, bp likes to have it like that
return const_cast(p.get());
}
}
}
#include
#include
namespace boost {
namespace python {
template struct pointee > {
typedef T type;
};
}
}
#include
using namespace FastNoise;
using namespace boost::python;
namespace np = boost::python::numpy;
// auto fnSimplex = FastNoise::New();
template
static SmartNode NewNode()
{
return New();
}
template
static void SetSource(T* dest_node, Generator* src_node)
{
SmartNode sptr;
sptr.reset(src_node);
dest_node->SetSource(sptr);
}
static void SetDomainOffsetFloat(DomainOffset* self, Dim dim, float value)
{
switch(dim)
{
case Dim::X: return self->SetOffset(value);
case Dim::Y: return self->SetOffset(value);
case Dim::Z: return self->SetOffset(value);
default: return self->SetOffset(value);
}
}
static void SetDomainOffsetSource(DomainOffset* self, Dim dim, Generator* src_node)
{
SmartNode sptr;
sptr.reset(src_node);
switch(dim)
{
case Dim::X: return self->SetOffset(sptr);
case Dim::Y: return self->SetOffset(sptr);
case Dim::Z: return self->SetOffset(sptr);
default: return self->SetOffset(sptr);
}
}
static void SetDomainAxisScale(DomainAxisScale* self, Dim dim, float value)
{
switch(dim)
{
case Dim::X: return self->SetScale(value);
case Dim::Y: return self->SetScale(value);
case Dim::Z: return self->SetScale(value);
default: return self->SetScale(value);
}
}
static void SetNewDimensionPosition(AddDimension* self, Generator* src_node)
{
SmartNode sptr;
sptr.reset(src_node);
self->SetNewDimensionPosition(sptr);
}
static void FractalSetGain(Fractal<>* self, Generator* src_node)
{
SmartNode sptr;
sptr.reset(src_node);
self->SetGain(sptr);
}
static void FractalSetWeightedStrength(Fractal<>* self, Generator* src_node)
{
SmartNode sptr;
sptr.reset(src_node);
self->SetWeightedStrength(sptr);
}
static void FractalSetPingPongStrength(FractalPingPong* self, Generator* src_node)
{
SmartNode sptr;
sptr.reset(src_node);
self->SetPingPongStrength(sptr);
}
static void CellularSetJitterModifier(Cellular* self, Generator* src_node)
{
SmartNode sptr;
sptr.reset(src_node);
self->SetJitterModifier(sptr);
}
static void CellularSetLookup(CellularLookup* self, Generator* src_node)
{
SmartNode sptr;
sptr.reset(src_node);
self->SetLookup(sptr);
}
// cf. https://cosmiccoding.com.au/tutorials/boost
static tuple GenUniformGrid2D(Generator* self, np::ndarray & array,
int xStart, int yStart,
int xSize, int ySize,
float frequency, int seed )
{
// Make sure we get doubles
if (array.get_dtype() != np::dtype::get_builtin()) {
PyErr_SetString(PyExc_TypeError, "Incorrect array data type");
throw_error_already_set();
}
float* data = reinterpret_cast(array.get_data());
OutputMinMax res = self->GenUniformGrid2D(data, xStart, yStart, xSize, ySize, frequency, seed);
tuple minmax = make_tuple(res.min, res.max);
return minmax;
}
BOOST_PYTHON_MODULE(pyfn2)
{
Py_Initialize();
np::initialize();
def("hello", hello);
enum_("eLevel")
.value("Null", FastSIMD::Level_Null)
.value("Scalar", FastSIMD::Level_Scalar)
.value("SSE", FastSIMD::Level_SSE)
.value("SSE2", FastSIMD::Level_SSE2)
.value("SSE3", FastSIMD::Level_SSE3)
.value("SSSE3", FastSIMD::Level_SSSE3)
.value("SSE41", FastSIMD::Level_SSE41)
.value("SSE42", FastSIMD::Level_SSE42)
.value("AVX", FastSIMD::Level_AVX)
.value("AVX2", FastSIMD::Level_AVX2)
.value("AVX512", FastSIMD::Level_AVX512)
.value("NEON", FastSIMD::Level_NEON);
enum_("Dim")
.value("X", FastNoise::Dim::X)
.value("Y", FastNoise::Dim::Y)
.value("Z", FastNoise::Dim::Z)
.value("W", FastNoise::Dim::W);
enum_("DistanceFunction")
.value("Euclidean", FastNoise::DistanceFunction::Euclidean)
.value("EuclideanSquared", FastNoise::DistanceFunction::EuclideanSquared)
.value("Manhattan", FastNoise::DistanceFunction::Manhattan)
.value("Hybrid", FastNoise::DistanceFunction::Hybrid)
.value("MaxAxis", FastNoise::DistanceFunction::MaxAxis);
enum_("CellDistReturnType")
.value("Index0", CellularDistance::ReturnType::Index0)
.value("Index0Add1", CellularDistance::ReturnType::Index0Add1)
.value("Index0Sub1", CellularDistance::ReturnType::Index0Sub1)
.value("Index0Mul1", CellularDistance::ReturnType::Index0Mul1)
.value("Index0Div1", CellularDistance::ReturnType::Index0Div1);
class_, boost::noncopyable>("Generator", no_init)
.def("GetSIMDLevel", &Generator::GetSIMDLevel)
.def("GenUniformGrid2D", &GenUniformGrid2D)
;
class_, bases, boost::noncopyable>("Simplex", no_init)
.def("New", &NewNode).staticmethod("New")
;
class_, bases, boost::noncopyable>("OpenSimplex2", no_init)
.def("New", &NewNode).staticmethod("New")
;
class_, bases, boost::noncopyable>("Perlin", no_init)
.def("New", &NewNode).staticmethod("New")
;
class_, bases, boost::noncopyable>("Value", no_init)
.def("New", &NewNode).staticmethod("New")
;
class_, bases, boost::noncopyable>("DomainScale", no_init)
.def("New", &NewNode).staticmethod("New")
.def("SetSource", &SetSource)
.def("SetScale", &DomainScale::SetScale)
;
class_, bases, boost::noncopyable>("DomainOffset", no_init)
.def("New", &NewNode).staticmethod("New")
.def("SetSource", &SetSource)
.def("SetOffset", &SetDomainOffsetFloat)
.def("SetOffset", &SetDomainOffsetSource)
;
class_, bases, boost::noncopyable>("DomainRotate", no_init)
.def("New", &NewNode).staticmethod("New")
.def("SetSource", &SetSource)
.def("SetYaw", &DomainRotate::SetYaw)
.def("SetPitch", &DomainRotate::SetPitch)
.def("SetRoll", &DomainRotate::SetRoll)
;
class_, bases, boost::noncopyable>("SeedOffset", no_init)
.def("New", &NewNode).staticmethod("New")
.def("SetSource", &SetSource)
.def("SetOffset", &SeedOffset::SetOffset)
;
class_, bases, boost::noncopyable>("Remap", no_init)
.def("New", &NewNode).staticmethod("New")
.def("SetSource", &SetSource)
.def("SetRemap", &Remap::SetRemap)
;
class_, bases, boost::noncopyable>("ConvertRGBA8", no_init)
.def("New", &NewNode).staticmethod("New")
.def("SetSource", &SetSource)
.def("SetMinMax", &ConvertRGBA8::SetMinMax)
;
class_, bases, boost::noncopyable>("Terrace", no_init)
.def("New", &NewNode).staticmethod("New")
.def("SetSource", &SetSource)
.def("SetMultiplier", &Terrace::SetMultiplier)
.def("SetSmoothness", &Terrace::SetSmoothness)
;
class_, bases, boost::noncopyable>("DomainAxisScale", no_init)
.def("New", &NewNode).staticmethod("New")
.def("SetSource", &SetSource)
.def("SetScale", &SetDomainAxisScale)
;
class_, bases, boost::noncopyable>("AddDimension", no_init)
.def("New", &NewNode).staticmethod("New")
.def("SetSource", &SetSource)
.def("SetNewDimensionPosition", &AddDimension::SetNewDimensionPosition)
.def("SetNewDimensionPosition", &SetNewDimensionPosition)
;
class_, bases, boost::noncopyable>("RemoveDimension", no_init)
.def("New", &NewNode).staticmethod("New")
.def("SetSource", &SetSource)
.def("SetRemoveDimension", &RemoveDimension::SetRemoveDimension)
;
class_, bases, boost::noncopyable>("GeneratorCache", no_init)
.def("New", &NewNode).staticmethod("New")
.def("SetSource", &SetSource)
;
class_, SmartNode>, bases, boost::noncopyable>("Fractal", no_init)
// .def("New", &NewNode>).staticmethod("New")
.def("SetSource", &SetSource>)
.def::*)(float)>("SetGain", &Fractal<>::SetGain)
.def("SetGain", &FractalSetGain)
.def::*)(float)>("SetWeightedStrength", &Fractal<>::SetWeightedStrength)
.def("SetWeightedStrength", &FractalSetWeightedStrength)
.def("SetOctaveCount", &Fractal<>::SetOctaveCount)
.def("SetLacunarity", &Fractal<>::SetLacunarity)
;
class_, bases>, boost::noncopyable>("FractalFBm", no_init)
.def("New", &NewNode).staticmethod("New")
;
class_, bases>, boost::noncopyable>("FractalRidged", no_init)
.def("New", &NewNode).staticmethod("New")
;
class_, bases>, boost::noncopyable>("FractalPingPong", no_init)
.def("New", &NewNode).staticmethod("New")
.def("SetPingPongStrength", &FractalPingPong::SetPingPongStrength)
.def("SetPingPongStrength", &FractalSetPingPongStrength)
;
class_, bases, boost::noncopyable>("Cellular", no_init)
// .def("New", &NewNode).staticmethod("New")
.def("SetJitterModifier", &Cellular::SetJitterModifier)
.def("SetJitterModifier", &CellularSetJitterModifier)
.def("SetDistanceFunction", &Cellular::SetDistanceFunction)
;
class_, bases, boost::noncopyable>("CellularValue", no_init)
.def("New", &NewNode).staticmethod("New")
.def("SetValueIndex", &CellularValue::SetValueIndex)
;
class_, bases, boost::noncopyable>("CellularDistance", no_init)
.def("New", &NewNode).staticmethod("New")
.def("SetDistanceIndex0", &CellularDistance::SetDistanceIndex0)
.def("SetDistanceIndex1", &CellularDistance::SetDistanceIndex1)
.def("SetReturnType", &CellularDistance::SetReturnType)
;
class_, bases, boost::noncopyable>("CellularLookup", no_init)
.def("New", &NewNode).staticmethod("New")
.def("SetLookup", &CellularSetLookup)
.def("SetLookupFrequency", &CellularLookup::SetLookupFrequency)
;
// implicitly_convertible< SmartNode, SmartNode >();
// implicitly_convertible< SmartNode, SmartNode >();
// implicitly_convertible< SmartNode, SmartNode >();
// implicitly_convertible< SmartNode, SmartNode >();
// implicitly_convertible< SmartNode, SmartNode >();
};
* And I can successfully run 24 small unit tests with pytest to create those nodes: """Test module for pyfn2 module"""
# import sys
# print(sys.path)
import numpy as np
from dist.bin import pyfn2
class TestBindings():
"""Test class pyfn2 bindings"""
def test_sanity(self):
"""Sanity check"""
assert 1 == 1
def test_hello(self):
"""Test simple hello function"""
pyfn2.hello()
def test_simplex(self):
"""Test create a simplex object"""
node = pyfn2.Simplex.New()
# get the SIMD level:
lvl = node.GetSIMDLevel()
print(f"Simplex SIMD level: {lvl}")
assert lvl >= 0
def test_opensimplex2(self):
"""Test create a opensimplex2 object"""
node = pyfn2.OpenSimplex2.New()
lvl = node.GetSIMDLevel()
print(f"OpenSimplex2 SIMD level: {lvl}")
assert lvl >= 0
def test_perlin(self):
"""Test create a Perlin object"""
node = pyfn2.Perlin.New()
lvl = node.GetSIMDLevel()
print(f"Perlin SIMD level: {lvl}")
assert lvl >= 0
def test_value(self):
"""Test create a Value object"""
node = pyfn2.Value.New()
lvl = node.GetSIMDLevel()
print(f"Value SIMD level: {lvl}")
assert lvl >= 0
def test_domain_scale(self):
"""Test create a DomainScale object"""
node = pyfn2.DomainScale.New()
src = pyfn2.Simplex.New()
node.SetSource(src)
node.SetScale(3.0)
lvl = node.GetSIMDLevel()
print(f"DomainScale SIMD level: {lvl}")
assert lvl >= 0
def test_domain_offset(self):
"""Test create a DomainOffset object"""
node = pyfn2.DomainOffset.New()
src = pyfn2.Simplex.New()
src2 = pyfn2.Perlin.New()
node.SetSource(src)
node.SetOffset(pyfn2.Dim.X, 2.0)
node.SetOffset(pyfn2.Dim.Y, 2.5)
node.SetOffset(pyfn2.Dim.Z, 3.5)
node.SetOffset(pyfn2.Dim.Z, src2)
lvl = node.GetSIMDLevel()
print(f"DomainOffset SIMD level: {lvl}")
assert lvl >= 0
def test_domain_rotate(self):
"""Test create a DomainRotate object"""
node = pyfn2.DomainRotate.New()
src = pyfn2.Simplex.New()
node.SetSource(src)
node.SetYaw(0.1)
node.SetPitch(0.2)
node.SetRoll(0.3)
lvl = node.GetSIMDLevel()
print(f"DomainRotate SIMD level: {lvl}")
assert lvl >= 0
def test_seed_offset(self):
"""Test create a SeedOffset object"""
node = pyfn2.SeedOffset.New()
src = pyfn2.Simplex.New()
node.SetSource(src)
node.SetOffset(3)
lvl = node.GetSIMDLevel()
print(f"SeedOffset SIMD level: {lvl}")
assert lvl >= 0
def test_remap(self):
"""Test create a Remap object"""
node = pyfn2.Remap.New()
src = pyfn2.Simplex.New()
node.SetSource(src)
node.SetRemap(-0.5, 0.5, 0.0, 3.0)
lvl = node.GetSIMDLevel()
print(f"Remap SIMD level: {lvl}")
assert lvl >= 0
def test_convert_rgba8(self):
"""Test create a ConvertRGBA8 object"""
node = pyfn2.ConvertRGBA8.New()
src = pyfn2.Simplex.New()
node.SetSource(src)
node.SetMinMax(0.0, 0.8)
lvl = node.GetSIMDLevel()
print(f"ConvertRGBA8 SIMD level: {lvl}")
assert lvl >= 0
def test_terrace(self):
"""Test create a Terrace object"""
node = pyfn2.Terrace.New()
src = pyfn2.Simplex.New()
node.SetSource(src)
node.SetMultiplier(3.0)
node.SetSmoothness(0.4)
lvl = node.GetSIMDLevel()
print(f"Terrace SIMD level: {lvl}")
assert lvl >= 0
def test_domain_axis_scale(self):
"""Test create a DomainAxisScale object"""
node = pyfn2.DomainAxisScale.New()
src = pyfn2.Simplex.New()
node.SetSource(src)
node.SetScale(pyfn2.Dim.X, 2.0)
node.SetScale(pyfn2.Dim.Y, 2.5)
node.SetScale(pyfn2.Dim.Z, 3.5)
node.SetScale(pyfn2.Dim.W, 4.5)
lvl = node.GetSIMDLevel()
print(f"DomainAxisScale SIMD level: {lvl}")
assert lvl >= 0
def test_add_dimension(self):
"""Test create a AddDimension object"""
node = pyfn2.AddDimension.New()
src = pyfn2.Simplex.New()
src2 = pyfn2.Perlin.New()
node.SetSource(src)
node.SetNewDimensionPosition(3.0)
node.SetNewDimensionPosition(src2)
lvl = node.GetSIMDLevel()
print(f"AddDimension SIMD level: {lvl}")
assert lvl >= 0
def test_remove_dimension(self):
"""Test create a RemoveDimension object"""
node = pyfn2.RemoveDimension.New()
src = pyfn2.Simplex.New()
node.SetSource(src)
node.SetRemoveDimension(pyfn2.Dim.W)
lvl = node.GetSIMDLevel()
print(f"RemoveDimension SIMD level: {lvl}")
assert lvl >= 0
def test_generator_cache(self):
"""Test create a GeneratorCache object"""
node = pyfn2.GeneratorCache.New()
src = pyfn2.Simplex.New()
node.SetSource(src)
lvl = node.GetSIMDLevel()
print(f"GeneratorCache SIMD level: {lvl}")
assert lvl >= 0
def test_generator_gen2d(self):
"""Test generate on 2d grid"""
arr = np.zeros((10, 10), dtype=np.float32)
node = pyfn2.Simplex.New()
offset = pyfn2.DomainOffset.New()
offset.SetSource(node)
offset.SetOffset(pyfn2.Dim.X, 2.0)
offset.SetOffset(pyfn2.Dim.Y, 2.0)
nrange = offset.GenUniformGrid2D(arr, 0, 0, 10, 10, 1.0, 123)
# node.GenUniformGrid2D(0, 0, 10, 10, 0.1, 123)
print(f"Generated array is: {arr}")
print(f"Range is: {nrange}")
mini = np.amin(arr)
maxi = np.amax(arr)
assert nrange[0] == mini
assert nrange[1] == maxi
def test_fractal_fbm(self):
"""Test generate a fractal FBm"""
node = pyfn2.FractalFBm.New()
src = pyfn2.Simplex.New()
node.SetSource(src)
node.SetGain(3.0)
node.SetGain(pyfn2.Perlin.New())
node.SetWeightedStrength(1.2)
node.SetWeightedStrength(pyfn2.Value.New())
node.SetOctaveCount(10)
node.SetLacunarity(0.5)
def test_fractal_ridged(self):
"""Test generate a fractal Ridged"""
node = pyfn2.FractalRidged.New()
src = pyfn2.Simplex.New()
node.SetSource(src)
node.SetGain(3.0)
node.SetGain(pyfn2.Perlin.New())
node.SetWeightedStrength(1.2)
node.SetWeightedStrength(pyfn2.Value.New())
node.SetOctaveCount(10)
node.SetLacunarity(0.5)
def test_fractal_pingpong(self):
"""Test generate a fractal PingPong"""
node = pyfn2.FractalPingPong.New()
src = pyfn2.Simplex.New()
node.SetSource(src)
node.SetGain(3.0)
node.SetGain(pyfn2.Perlin.New())
node.SetWeightedStrength(1.2)
node.SetWeightedStrength(pyfn2.Value.New())
node.SetOctaveCount(10)
node.SetLacunarity(0.5)
def test_cellular_value(self):
"""Test generate a cellular value"""
node = pyfn2.CellularValue.New()
node.SetJitterModifier(3.0)
node.SetJitterModifier(pyfn2.Perlin.New())
node.SetDistanceFunction(pyfn2.DistanceFunction.Euclidean)
node.SetValueIndex(2)
def test_cellular_distance(self):
"""Test generate a cellular distance"""
node = pyfn2.CellularDistance.New()
node.SetJitterModifier(3.0)
node.SetJitterModifier(pyfn2.Perlin.New())
node.SetDistanceFunction(pyfn2.DistanceFunction.Euclidean)
node.SetDistanceIndex0(1)
node.SetDistanceIndex1(0)
node.SetReturnType(pyfn2.CellDistReturnType.Index0Add1)
def test_cellular_lookup(self):
"""Test generate a cellular lookup"""
node = pyfn2.CellularLookup.New()
node.SetJitterModifier(3.0)
node.SetJitterModifier(pyfn2.Perlin.New())
node.SetDistanceFunction(pyfn2.DistanceFunction.Euclidean)
node.SetLookup(pyfn2.Perlin.New())
node.SetLookupFrequency(0.5)
Those files above are available as part of the [[https://github.com/roche-emmanuel/nervproj|NervProj github project]] if anyone needs a direct access to that code.
* **Note**: This first version will compile and the test will run fine for me on both Windows and Linux, I could not try anything on MAC since I don't have one ๐.
===== Conclusion =====
Those FastNoise2 python bindings are **not complete yet**, but I think this is good enough for me to try to use them in another python project where I need to generate some dummy inputs, and this was my main interest here. So I'll leave it at this point for the moment and come back to it if I have the time later ๐!
**Note**: concerning the initial target of "building a GUI" to generate noise maps, I'm still somewhat interested in that, but again, that's not my absolute priority here, and I'm a bit in an hurry for the moment unfortunately, so i'll see later if I get a chance to do it (if it can really help me in day to day my work)