====== 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)