Search code examples
coldfusioncfmlperlin-noiselucee

What is the easiest method of generating perlin noise in Lucee?


I'm writing a simple web-based game that requires me to create random overworld 'zones' consisting of a few thousand terrain tiles (probably between 100x100 and 500x500). Most advice online suggests that I start by generating perlin noise and using that as an altitude map, followed by another instance for moisture, another for temperature, and so on, and then assign terrain values based on a combination of those.

I would prefer not to rely on installing any other languages or programs to do this. However, there doesn't appear to be any built-in function to generate a perlin noise map with CFML directly. What is the easiest way to do this with the minimum external dependencies?

Is there some "perlinNoise" java method I can make use of to build an array that I can then work with in CFML? Is there cfscript/cfml source code or a cfc available online to implement a perlin function (I don't know if I can translate something from another language myself)? Or would the easiest route be installing and using something like ImageMagick to generate/read an image file via cfexecute?

What I've Tried

I first attempted to convert the C++ code shown on wikipedia. This would probably be easy, if I had ever worked with C++ in my life. Unfortunately, I have not. I got as far as this:

<cffunction name="lerp" access="public" output="no" returntype="numeric" description="Function to linearly interpolate between a0 and a1">
    <cfargument name="a0"       type="numeric"  required="yes">
    <cfargument name="a1"       type="numeric"  required="yes">
    <cfargument name="weight"   type="numeric"  required="yes">

    <cfset returnVal    = (1.0 - weight) * a0 + weight * a1>

    <cfreturn returnVal>
</cffunction>

<cffunction name="dotGridGradient" access="public" output="no" returntype="numeric" description="Computes the dot product of the distance and gradient vectors.">
    <cfargument name="ix"   type="numeric"  required="yes">
    <cfargument name="iy"   type="numeric"  required="yes">
    <cfargument name="x"    type="numeric"  required="yes">
    <cfargument name="y"    type="numeric"  required="yes">

    <!--- Precomputed (or otherwise) gradient vectors at each grid node --->
    <!--- <cfset test       = Gradient[IYMAX][IXMAX][2]> --->

    <!--- Compute the distance vector --->
    <cfset dx       = x - ix>
    <cfset dy       = y - iy>

    <!--- Compute the dot-product --->
    <cfset returnVal= (dx*Gradient[iy][ix][0] + dy*Gradient[iy][ix][1])>

    <cfreturn returnVal>
</cffunction>

<cffunction name="perlin" access="public" output="no" returntype="numeric" description="Compute Perlin noise at coordinates x, y">
    <cfargument name="x"        type="numeric"  required="yes">
    <cfargument name="y"        type="numeric"  required="yes">

    <!--- Determine grid cell coordinates --->
    <cfset x1       = int(x) + 1>
    <cfset y1       = int(y) + 1>

    <!--- Determine interpolation weights --->
    <!--- Could also use higher order polynomial/s-curve here --->
    <cfset sx       = x - x0>
    <cfset sy       = y - y0>

    <!--- Interpolate between grid point gradients --->
    float n0, n1, ix0, ix1, value;
    <cfset n0       = dotGridGradient(x0, y0, x, y)>
    <cfset n1       = dotGridGradient(x1, y0, x, y)>
    <cfset ix0      = lerp(n0, n1, sx)>
    <cfset n0       = dotGridGradient(x0, y1, x, y)>
    <cfset n1       = dotGridGradient(x1, y1, x, y)>
    <cfset ix1      = lerp(n0, n1, sx)>
    <cfset returnVal= lerp(ix0, ix1, sy)>

    <cfreturn returnVal>
</cffunction>

However, only the lerp function actually runs. I had no idea what 'gradient' means. I assume it's a math function, but I'm not sure how to implement it here. My Google searches keep giving me different code for it, along with some explanations that aren't intuitive to me.

At this point using IM was getting far more appealing. It seems more powerful, and I was just avoiding it as figuring it out and having one more thing to install on every server move seemed like more work than having something all in code. With the code approach seeming more complicated than I anticipated, I took a break to try focusing on IM.

For that I started by creating a seeded plasma or fractal canvas, which worked great. I then tried many different methods to pull information for each pixel with limited success:

    <cfexecute  name="#ImageMagick#\magick.exe"
                variable="imgResult"
                timeout="60"
                arguments="convert -size 500x500 -seed #seed# plasma:fractal -blur #blur# -shade 120x45 -auto-level #imgRoot#/temp/#fname#.png" />

    <cfloop from="1" to="20" index="x">
        <cfloop from="1" to="20" index="y">
            <!--- <cfexecute    name="#ImageMagick#\magick.exe"
                        variable="imgResult"
                        timeout="60"
                        arguments="convert '#imgRoot#/temp/#fname#.png[1x1+#x#+#y#]' #imgRoot#/temp/temp.png" /> --->

            <!--- Works; takes 27s for 400 pixels.  Will take hours for full size maps.
            <cfexecute  name="#ImageMagick#\magick.exe"
                        variable="imgResult"
                        timeout="60"
                        arguments="identify -verbose #imgRoot#/temp/#fname#.png[1x1+#x#+#y#]" />
            <cfset imgResult    = ListFirst(ListLast(imgResult, "gray("), "%")>
            --->

            <!--- Returns blank; probably because of u.r not being defined in a grayscale image? 
            <cfexecute  name="#ImageMagick#\magick.exe"
                        variable="imgResult"
                        timeout="60"
                        arguments="convert #imgRoot#/temp/#fname#.png[1x1+#x#+#y#] -format ""%[fx:floor(255*u)]"" info" />
            --->

            <!--- Errors with some decode delegate error
            <cfexecute  name="#ImageMagick#\magick.exe"
                        variable="imgResult"
                        timeout="60"
                        arguments="convert #imgRoot#/temp/#fname#.png: -format '%[pixel:p{#x#,#y#}]' info" /> --->
            <!--- Errors with some decode delegate error
            <cfexecute  name="#ImageMagick#\magick.exe"
                        variable="imgResult"
                        timeout="60"
                        arguments="convert #imgRoot#/temp/#fname#.png: -crop 1x1+#x#+#y# -depth 8 txt" />
                         --->

            <!--- Returns the same value for every pixel
            <cfexecute  name="#ImageMagick#\magick.exe"
                        variable="imgResult"
                        timeout="60"
                        arguments="convert -verbose #imgRoot#/temp/#fname#.png[1x1+#x#+#y#] txt" />
                         --->

            <cfexecute  name="#ImageMagick#\magick.exe"
                        variable="imgResult"
                        timeout="60"
                        arguments="identify -verbose #imgRoot#/temp/#fname#.png[1x1+#x#+#y#]" />
            <cfset imgResult    = ListFirst(ListLast(imgResult, "gray("), "%")>
            <cfset returnVal[x][y]  = imgResult>
        </cfloop>
    </cfloop>

So my best method so far is requiring 27s to pull data for 400 pixels, and that's without doing anything with that data. If I should need to process a 160k pixel image (400x400) in a real world scenario, that works out to approximately 3 hours of pegging my processor. So assuming I need 3 maps (for altitude, humidity, and temperature), that's...impractical.


Solution

  • I've not been able to find a solution I'm completely happy with in terms of efficiency, but I've run out of time to solve this and move on. I will likely come back in the future to optimize, but for now, I have a solution that works, albeit slowly.

    Per Mark Setchell's excellent answer at https://stackoverflow.com/a/26629083/762721, I've found that, surprisingly, the most efficient approach to my problem is to generate a fractal with Image Magic, use IM to write all of the color information out to a file, and then use Lucee to read that file in and parse each line for luminosity information. Here is the code I'm using:

        <cfexecute  name="#ImageMagick#\magick.exe"
                    variable="imgResult"
                    timeout="60"
                    arguments="convert -size 500x500 -seed #seed# plasma:fractal -blur #blur# -shade 120x45 -auto-level #imgRoot#/temp/#fname#.png" />
    
        <cfexecute  name="#ImageMagick#\magick.exe"
                    variable="imgResult"
                    timeout="60"
                    arguments="convert #imgRoot#/temp/#fname#.png -depth 8 #imgRoot#/temp/test.txt" />
    
        <cfset myfile       = FileOpen("#imgRoot#/temp/test.txt", "read")>
        <cfloop condition="NOT FileisEOF(myfile)">
            <cfset thisLine = FileReadLine(myfile)>
            <cfset x        = listFirst(thisLine, ",")>
            <cfset y        = listGetAt(thisLine, 2, ",")>
            <cfset y        = listFirst(y, ":")>
            <cfif isNumeric(x) and isNumeric(y)>
                <cfset thisStart    = FindNoCase("gray(", thisLine)>
                <cfif thisStart is not 0>
                    <cfset thisVal      = Mid(thisLine, thisStart+5, 99999)>
                    <cfset thisVal      = listFirst(thisVal, ")")>
                    <cfset returnVal[x+1][y+1] = "#thisVal#">
                </cfif>
            </cfif>
        </cfloop>
        <cfset FileClose(myfile)>
    

    I was able to run this on a 250k pixel image (500x500) in 7.1 minutes, making it almost 40 times faster than my attempt to get the pixel information directly. I think there's lots of room for both optimization, and validation to avoid errors, and once I tighten it up a bit I'll come back and update this answer.

    For now, using this to generate 3 500x500 images, parse the information, and write it to the database can be done in 30 minutes. Which while suboptimal, is practical.