Stuck. Attempt at creating an Infinite Treadmill Terrain with Away3D 5.0.9 (Code included)

So far, this is the best result I have been able to achieve…

(Haxe 4.3.6, Away3D 5.0.9, OpenFl 9.5.0, Lime 8.3.0, Hxcpp 4.3.2)

It’s supposed to be an heightmap based terrain, with volumes of course, but it’s all flat. Of course. But it scrolls and displays the texture. I can’t go any further. If someone wants to try and figure out how to achieve the final step, here’s the code.

Project.xml

<?xml version="1.0" encoding="utf-8"?>
<project>
	<meta title="InfiniteTerrain" package="com.example.terrain" version="1.0.0" company="User" />
	<app main="Main" file="InfiniteTerrain" path="bin" />
	
	<window width="1280" height="720" fps="60" hardware="true" allow-high-dpi="true" />
	
	<source path="src" />
	<haxelib name="openfl" />
	<haxelib name="away3d" />
	
	<assets path="assets" rename="assets" />
    
    <!-- Required for Vertex Texture Fetch (sampling textures in vertex shader) -->
    <window profile="baselineExtended" />
</project>

Main.hx

package;

import away3d.containers.View3D;
import away3d.lights.DirectionalLight;
import away3d.materials.lightpickers.StaticLightPicker;
import away3d.textures.BitmapTexture;
import openfl.display.Sprite;
import openfl.events.Event;
import openfl.utils.Assets;
import openfl.display.BitmapData;
import openfl.geom.Vector3D;

class Main extends Sprite {
    private var _view:View3D;
    private var _terrain:TerrainManager;

    public function new() {
        super();
        if (Assets.exists("assets/heightMap.jpg")) {
            Assets.loadBitmapData("assets/heightMap.jpg").onComplete(init);
        } else {
            // Fallback: Create a procedural green grid if file is missing
            var bmd = new BitmapData(512, 512, false, 0);
                bmd.perlinNoise(72, 72, 4, 12, true, true, 7, true);
            init(bmd);
        }
    }

    private function init(textureData:BitmapData):Void {
        _view = new View3D();
        _view.backgroundColor = 0x87CEEB;
        addChild(_view);

        _view.camera.lens.far = 250000;
        _view.camera.y = 6000;
        
        var light = new DirectionalLight(-0.5, -1, 0.5);
        light.ambient = 0.4;
        _view.scene.addChild(light);
        
        var lightPicker = new StaticLightPicker([light]);
        var texture = new BitmapTexture(textureData);
        
        _terrain = new TerrainManager(texture, lightPicker);
        _view.scene.addChild(_terrain.mesh);
        
        addEventListener(Event.ENTER_FRAME, onEnterFrame);
    }

    private function onEnterFrame(e:Event):Void {
        if (_view != null && _terrain != null) {
            // Continuous movement
            _view.camera.z += 100; 
            
            // Update terrain using camera position
            _terrain.update(_view.camera.position);
            _view.render();
        }
    }
}

TerrainManager.hx

package;

import away3d.entities.Mesh;
import away3d.materials.TextureMaterial;
import away3d.materials.lightpickers.StaticLightPicker;
import away3d.primitives.PlaneGeometry;
import away3d.textures.BitmapTexture;
import away3d.bounds.BoundingSphere;
import openfl.geom.Vector3D;

class TerrainManager {
    public var mesh:Mesh;
    private var _method:VertexDisplacementMethod;
    private var _gridSize:Float = 150000;

    public function new(texture:BitmapTexture, lightPicker:StaticLightPicker) {
        // High density (200 segments) makes mountains look like your image
        var geometry = new PlaneGeometry(_gridSize, _gridSize, 200, 200);
        
        _method = new VertexDisplacementMethod(50000); 
        
        var material = new TextureMaterial(texture);
        material.lightPicker = lightPicker;
        material.repeat = true;
        material.ambient = 0.5;
        material.addMethod(_method);
        
        mesh = new Mesh(geometry, material);
        mesh.bounds = new BoundingSphere();
        // Massively tall bounding box to never cull the volume
        mesh.bounds.fromExtremes(-200000, -100000, -200000, 200000, 100000, 200000);
    }

    public function update(cameraPos:Vector3D):Void {
        // Fix the mesh to the camera center. NO SNAPPING = NO JITTER.
        mesh.x = cameraPos.x;
        mesh.z = cameraPos.z;

        // Tell the shader where the camera is so it can offset the math
        _method.setCameraPosition(cameraPos.x, cameraPos.z);
    }
}

VertexDisplacementMethod.hx

package;

import away3d.materials.methods.EffectMethodBase;
import away3d.materials.methods.MethodVO;
import away3d.materials.compilation.ShaderRegisterCache;
import away3d.materials.compilation.ShaderRegisterElement;
import away3d.core.managers.Stage3DProxy;
import openfl.Vector;

class VertexDisplacementMethod extends EffectMethodBase {
    private var _camX:Float = 0;
    private var _camZ:Float = 0;
    private var _amplitude:Float;

    public function new(amplitude:Float = 40000) {
        super();
        _amplitude = amplitude;
    }

    public function setCameraPosition(x:Float, z:Float):Void {
        _camX = x;
        _camZ = z;
    }

    override public function initVO(vo:MethodVO):Void {
        vo.needsUV = true;
    }

    override public function activate(vo:MethodVO, stage3DProxy:Stage3DProxy):Void {
        var vIndex:Int = vo.vertexConstantsIndex;
        if (vIndex != -1) {
            var vData:Vector<Float> = vo.vertexData;
            // vcN: [CameraX, CameraZ, Amplitude, TextureScale]
            vData[vIndex] = _camX;
            vData[vIndex + 1] = _camZ;
            vData[vIndex + 2] = _amplitude;
            vData[vIndex + 3] = 0.000008; // Huge scale for large terrain

            // vcN+1: [Freq1, Freq2, Freq3, DetailPersistence]
            vData[vIndex + 4] = 0.0001; 
            vData[vIndex + 5] = 0.0003;
            vData[vIndex + 6] = 0.0009;
            vData[vIndex + 7] = 0.45;
        }
    }

    override public function getVertexCode(vo:MethodVO, regCache:ShaderRegisterCache):String {
        var dataReg:ShaderRegisterElement = regCache.getFreeVertexConstant();
        var mathReg:ShaderRegisterElement = regCache.getFreeVertexConstant();
        vo.vertexConstantsIndex = dataReg.index * 4;

        var world:ShaderRegisterElement = regCache.getFreeVertexVectorTemp();
        var hReg:ShaderRegisterElement = regCache.getFreeVertexVectorTemp();
        var tReg:ShaderRegisterElement = regCache.getFreeVertexVectorTemp();
        
        var code:String = "";
        
        // 1. CALCULATE WORLD POS: Camera + Local
        code += "add " + world + ".x, vt0.x, " + dataReg + ".x \n";
        code += "add " + world + ".z, vt0.z, " + dataReg + ".y \n";
        
        // 2. PROJECT UVs: Makes the texture stay pinned to the world
        code += "mul v0.xy, " + world + ".xz, " + dataReg + ".w \n";
        
        // 3. FRACTAL MOUNTAINS (Ridged Noise Math)
        // Octave 1: Large Hills
        code += "mul " + tReg + ".xz, " + world + ".xz, " + mathReg + ".x \n";
        code += "sin " + hReg + ".x, " + tReg + ".x \n";
        code += "cos " + hReg + ".y, " + tReg + ".z \n";
        code += "mul " + hReg + ".z, " + hReg + ".x, " + hReg + ".y \n";
        code += "abs " + hReg + ".z, " + hReg + ".z \n"; // Sharp peaks
        
        // Octave 2: Ridges
        code += "mul " + tReg + ".xz, " + world + ".xz, " + mathReg + ".y \n";
        code += "sin " + tReg + ".x, " + tReg + ".x \n";
        code += "cos " + tReg + ".y, " + tReg + ".z \n";
        code += "mul " + tReg + ".x, " + tReg + ".x, " + tReg + ".y \n";
        code += "abs " + tReg + ".x, " + tReg + ".x \n";
        code += "mul " + tReg + ".x, " + tReg + ".x, " + mathReg + ".w \n"; // Apply persistence
        code += "add " + hReg + ".z, " + hReg + ".z, " + tReg + ".x \n";
        
        // 4. APPLY DISPLACEMENT
        code += "mul " + hReg + ".z, " + hReg + ".z, " + dataReg + ".z \n";
        code += "add vt0.y, vt0.y, " + hReg + ".z \n"; 
        
        return code;
    }

    override public function getFragmentCode(vo:MethodVO, regCache:ShaderRegisterCache, targetReg:ShaderRegisterElement):String {
        return ""; 
    }
}

Good luck.

This open pull request ( Allow effect methods to modify vertex position. by player-03 · Pull Request #67 · openfl/away3d · GitHub ) from @player_03 seems to suggest that effect methods cannot alter vertices.

This is way above my head though, so I could be wrong. Curious to hear what others think.

1 Like