blog:2022:0502_fastnoise2_python_bindings

Fast Noise generation in python with 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 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!

⇒ 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 👍!

  • 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)
  • 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<char>, 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 <module>
        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 🤪
  • 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"<ranlib>\"{comp_dir}/llvm-ranlib.exe\" ")
                    file.write(f"<archiver>\"{comp_dir}/llvm-ar.exe\" ")
                    file.write("<cxxflags>-D_CRT_SECURE_NO_WARNINGS ")
                    # file.write(f"<cxxflags>-D_SILENCE_CXX17_OLD_ALLOCATOR_MEMBERS_DEPRECATION_WARNING ")
                    file.write(";\n")
                else:
                    file.write(f"<compileflags>\"{cxxflags} -fPIC\" ")
                    file.write(f"<linkflags>\"{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)
    
  • 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.
  • 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 👍!
  • Now let's add some initial bindings in that pyfn2 C++ module
  • ⇒ 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 <module>
        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.
  • 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
    }
  • 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 <iostream>
    
    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 <typename T>
        class SmartNode;
    }
    
    // // here comes the magic
    // template <typename T> T* get_pointer(FastNoise::SmartNode<T> const& p) {
    //     //notice the const_cast<> at this point
    //     //for some unknown reason, bp likes to have it like that
    //     return const_cast<T*>(p.get());
    // }
    
    // some boost.python plumbing is required as you already know
    namespace boost {
        namespace python {
    
        // here comes the magic
        template <typename T> T* get_pointer(FastNoise::SmartNode<T> const& p) {
            //notice the const_cast<> at this point
            //for some unknown reason, bp likes to have it like that
            return const_cast<T*>(p.get());
        }
    
        } 
    }
    
    #include <boost/python.hpp>
    #include <boost/python/numpy.hpp>
    
    namespace boost {
        namespace python {
    
        template <typename T> struct pointee<FastNoise::SmartNode<T> > {
            typedef T type;
        };
    
        }
    }
    
    #include <FastNoise/FastNoise.h>
    using namespace FastNoise;
    using namespace boost::python;
    namespace np = boost::python::numpy;
    
    // auto fnSimplex = FastNoise::New<FastNoise::Simplex>();
    
    template<typename T>
    static SmartNode<T> NewNode()
    {
        return New<T>();
    }
    
    template<typename T>
    static void SetSource(T* dest_node, Generator* src_node)
    {
        SmartNode<Generator> 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<Dim::X>(value);
        case Dim::Y: return self->SetOffset<Dim::Y>(value);
        case Dim::Z: return self->SetOffset<Dim::Z>(value);
        default: return self->SetOffset<Dim::W>(value);
        }
    }
    
    static void SetDomainOffsetSource(DomainOffset* self, Dim dim, Generator* src_node)
    {
        SmartNode<Generator> sptr;
        sptr.reset(src_node);
    
        switch(dim)
        {
        case Dim::X: return self->SetOffset<Dim::X>(sptr);
        case Dim::Y: return self->SetOffset<Dim::Y>(sptr);
        case Dim::Z: return self->SetOffset<Dim::Z>(sptr);
        default: return self->SetOffset<Dim::W>(sptr);
        }
    }
    
    static void SetDomainAxisScale(DomainAxisScale* self, Dim dim, float value)
    {
        switch(dim)
        {
        case Dim::X: return self->SetScale<Dim::X>(value);
        case Dim::Y: return self->SetScale<Dim::Y>(value);
        case Dim::Z: return self->SetScale<Dim::Z>(value);
        default: return self->SetScale<Dim::W>(value);
        }
    }
    
    static void SetNewDimensionPosition(AddDimension* self, Generator* src_node)
    {
        SmartNode<Generator> sptr;
        sptr.reset(src_node);
        self->SetNewDimensionPosition(sptr);
    }
    
    static void FractalSetGain(Fractal<>* self, Generator* src_node)
    {
        SmartNode<Generator> sptr;
        sptr.reset(src_node);
        self->SetGain(sptr);
    }
    
    static void FractalSetWeightedStrength(Fractal<>* self, Generator* src_node)
    {
        SmartNode<Generator> sptr;
        sptr.reset(src_node);
        self->SetWeightedStrength(sptr);
    }
    
    static void FractalSetPingPongStrength(FractalPingPong* self, Generator* src_node)
    {
        SmartNode<Generator> sptr;
        sptr.reset(src_node);
        self->SetPingPongStrength(sptr);
    }
    
    static void CellularSetJitterModifier(Cellular* self, Generator* src_node)
    {
        SmartNode<Generator> sptr;
        sptr.reset(src_node);
        self->SetJitterModifier(sptr);
    }
    
    static void CellularSetLookup(CellularLookup* self, Generator* src_node)
    {
        SmartNode<Generator> 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<float>()) {
            PyErr_SetString(PyExc_TypeError, "Incorrect array data type");
            throw_error_already_set();
        }
        
        float* data = reinterpret_cast<float*>(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_<FastSIMD::eLevel>("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_<FastNoise::Dim>("Dim")
            .value("X", FastNoise::Dim::X)
            .value("Y", FastNoise::Dim::Y)
            .value("Z", FastNoise::Dim::Z)
            .value("W", FastNoise::Dim::W);
    
        enum_<FastNoise::DistanceFunction>("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_<FastNoise::CellularDistance::ReturnType>("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_<Generator, SmartNode<Generator>, boost::noncopyable>("Generator", no_init)
            .def("GetSIMDLevel", &Generator::GetSIMDLevel)
            .def("GenUniformGrid2D", &GenUniformGrid2D)
        ;
    
        class_<Simplex, SmartNode<Simplex>, bases<Generator>, boost::noncopyable>("Simplex", no_init)
            .def("New", &NewNode<Simplex>).staticmethod("New")
            ;
    
        class_<OpenSimplex2, SmartNode<OpenSimplex2>, bases<Generator>, boost::noncopyable>("OpenSimplex2", no_init)
            .def("New", &NewNode<OpenSimplex2>).staticmethod("New")
            ;
        
        class_<Perlin, SmartNode<Perlin>, bases<Generator>, boost::noncopyable>("Perlin", no_init)
            .def("New", &NewNode<Perlin>).staticmethod("New")
            ;
    
        class_<Value, SmartNode<Value>, bases<Generator>, boost::noncopyable>("Value", no_init)
            .def("New", &NewNode<Value>).staticmethod("New")
            ;
    
        class_<DomainScale, SmartNode<DomainScale>, bases<Generator>, boost::noncopyable>("DomainScale", no_init)
            .def("New", &NewNode<DomainScale>).staticmethod("New")
            .def("SetSource", &SetSource<DomainScale>)
            .def("SetScale", &DomainScale::SetScale)
            ;
    
        class_<DomainOffset, SmartNode<DomainOffset>, bases<Generator>, boost::noncopyable>("DomainOffset", no_init)
            .def("New", &NewNode<DomainOffset>).staticmethod("New")
            .def("SetSource", &SetSource<DomainOffset>)
            .def("SetOffset", &SetDomainOffsetFloat)
            .def("SetOffset", &SetDomainOffsetSource)
            ;
    
        class_<DomainRotate, SmartNode<DomainRotate>, bases<Generator>, boost::noncopyable>("DomainRotate", no_init)
            .def("New", &NewNode<DomainRotate>).staticmethod("New")
            .def("SetSource", &SetSource<DomainRotate>)
            .def("SetYaw", &DomainRotate::SetYaw)
            .def("SetPitch", &DomainRotate::SetPitch)
            .def("SetRoll", &DomainRotate::SetRoll)
            ;
    
        class_<SeedOffset, SmartNode<SeedOffset>, bases<Generator>, boost::noncopyable>("SeedOffset", no_init)
            .def("New", &NewNode<SeedOffset>).staticmethod("New")
            .def("SetSource", &SetSource<SeedOffset>)
            .def("SetOffset", &SeedOffset::SetOffset)
            ;
    
        class_<Remap, SmartNode<Remap>, bases<Generator>, boost::noncopyable>("Remap", no_init)
            .def("New", &NewNode<Remap>).staticmethod("New")
            .def("SetSource", &SetSource<Remap>)
            .def("SetRemap", &Remap::SetRemap)
            ;
    
        class_<ConvertRGBA8, SmartNode<ConvertRGBA8>, bases<Generator>, boost::noncopyable>("ConvertRGBA8", no_init)
            .def("New", &NewNode<ConvertRGBA8>).staticmethod("New")
            .def("SetSource", &SetSource<ConvertRGBA8>)
            .def("SetMinMax", &ConvertRGBA8::SetMinMax)
            ;
    
        class_<Terrace, SmartNode<Terrace>, bases<Generator>, boost::noncopyable>("Terrace", no_init)
            .def("New", &NewNode<Terrace>).staticmethod("New")
            .def("SetSource", &SetSource<Terrace>)
            .def("SetMultiplier", &Terrace::SetMultiplier)
            .def("SetSmoothness", &Terrace::SetSmoothness)
            ;
    
        class_<DomainAxisScale, SmartNode<DomainAxisScale>, bases<Generator>, boost::noncopyable>("DomainAxisScale", no_init)
            .def("New", &NewNode<DomainAxisScale>).staticmethod("New")
            .def("SetSource", &SetSource<DomainAxisScale>)
            .def("SetScale", &SetDomainAxisScale)
            ;
    
        class_<AddDimension, SmartNode<AddDimension>, bases<Generator>, boost::noncopyable>("AddDimension", no_init)
            .def("New", &NewNode<AddDimension>).staticmethod("New")
            .def("SetSource", &SetSource<AddDimension>)
            .def<void (AddDimension::*)(float)>("SetNewDimensionPosition", &AddDimension::SetNewDimensionPosition)
            .def("SetNewDimensionPosition", &SetNewDimensionPosition)
            ;
    
        class_<RemoveDimension, SmartNode<RemoveDimension>, bases<Generator>, boost::noncopyable>("RemoveDimension", no_init)
            .def("New", &NewNode<RemoveDimension>).staticmethod("New")
            .def("SetSource", &SetSource<RemoveDimension>)
            .def("SetRemoveDimension", &RemoveDimension::SetRemoveDimension)
            ;
    
        class_<GeneratorCache, SmartNode<GeneratorCache>, bases<Generator>, boost::noncopyable>("GeneratorCache", no_init)
            .def("New", &NewNode<GeneratorCache>).staticmethod("New")
            .def("SetSource", &SetSource<GeneratorCache>)
            ;
    
        class_<Fractal<>, SmartNode<Fractal<>>, bases<Generator>, boost::noncopyable>("Fractal", no_init)
            // .def("New", &NewNode<Fractal<>>).staticmethod("New")
            .def("SetSource", &SetSource<Fractal<>>)
            .def<void (Fractal<>::*)(float)>("SetGain", &Fractal<>::SetGain)
            .def("SetGain", &FractalSetGain)
            .def<void (Fractal<>::*)(float)>("SetWeightedStrength", &Fractal<>::SetWeightedStrength)
            .def("SetWeightedStrength", &FractalSetWeightedStrength)
            .def("SetOctaveCount", &Fractal<>::SetOctaveCount)
            .def("SetLacunarity", &Fractal<>::SetLacunarity)
            ;
    
        class_<FractalFBm, SmartNode<FractalFBm>, bases<Fractal<>>, boost::noncopyable>("FractalFBm", no_init)
            .def("New", &NewNode<FractalFBm>).staticmethod("New")
            ;
    
        class_<FractalRidged, SmartNode<FractalRidged>, bases<Fractal<>>, boost::noncopyable>("FractalRidged", no_init)
            .def("New", &NewNode<FractalRidged>).staticmethod("New")
            ;
    
        class_<FractalPingPong, SmartNode<FractalPingPong>, bases<Fractal<>>, boost::noncopyable>("FractalPingPong", no_init)
            .def("New", &NewNode<FractalPingPong>).staticmethod("New")
            .def<void (FractalPingPong::*)(float)>("SetPingPongStrength", &FractalPingPong::SetPingPongStrength)
            .def("SetPingPongStrength", &FractalSetPingPongStrength)
            ;
    
        class_<Cellular, SmartNode<Cellular>, bases<Generator>, boost::noncopyable>("Cellular", no_init)
            // .def("New", &NewNode<Cellular>).staticmethod("New")
            .def<void (Cellular::*)(float)>("SetJitterModifier", &Cellular::SetJitterModifier)
            .def("SetJitterModifier", &CellularSetJitterModifier)
            .def("SetDistanceFunction", &Cellular::SetDistanceFunction)
            ;
    
        class_<CellularValue, SmartNode<CellularValue>, bases<Cellular>, boost::noncopyable>("CellularValue", no_init)
            .def("New", &NewNode<CellularValue>).staticmethod("New")
            .def("SetValueIndex", &CellularValue::SetValueIndex)
            ;
    
        class_<CellularDistance, SmartNode<CellularDistance>, bases<Cellular>, boost::noncopyable>("CellularDistance", no_init)
            .def("New", &NewNode<CellularDistance>).staticmethod("New")
            .def("SetDistanceIndex0", &CellularDistance::SetDistanceIndex0)
            .def("SetDistanceIndex1", &CellularDistance::SetDistanceIndex1)
            .def("SetReturnType", &CellularDistance::SetReturnType)
            ;
    
    
        class_<CellularLookup, SmartNode<CellularLookup>, bases<Cellular>, boost::noncopyable>("CellularLookup", no_init)
            .def("New", &NewNode<CellularLookup>).staticmethod("New")
            .def("SetLookup", &CellularSetLookup)
            .def("SetLookupFrequency", &CellularLookup::SetLookupFrequency)
            ;
    
        // implicitly_convertible< SmartNode<Simplex>, SmartNode<Generator> >();
        // implicitly_convertible< SmartNode<OpenSimplex2>, SmartNode<Generator> >();
        // implicitly_convertible< SmartNode<Perlin>, SmartNode<Generator> >();
        // implicitly_convertible< SmartNode<Value>, SmartNode<Generator> >();
        // implicitly_convertible< SmartNode<DomainScale>, SmartNode<Generator> >();
    };
    
  • 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 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 😋.

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)

  • blog/2022/0502_fastnoise2_python_bindings.txt
  • Last modified: 2022/05/02 07:52
  • by 127.0.0.1