~firefoxsimulationslandscape
6 itemsDownload ./*

..
build
jsm
models
textures
index.gpu.tesselldation.old.html
index.html


landscapeindex.gpu.tesselldation.old.html
34 KB• 10•  2 weeks ago•  DownloadRawClose
2 weeks ago•  10

{}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Three.js Infinite Landscape</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
        #info {
            position: absolute;
            top: 10px;
            right: 10px;
            background: rgba(0, 0, 0, 0.5);
            color: white;
            padding: 10px;
            font-family: monospace;
            font-size: 12px;
            pointer-events: none;
        }
    </style>
</head>
<body>
    <div id="info"></div>
    <script type="importmap">
        {
            "imports": {
                "three": "./build/three.module.js",
                "three/addons/": "./jsm/"
            }
        }
    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

        // Classic Perlin Noise implementation (3D for terrain variation)
        class ClassicalNoise {
            constructor(r = Math) {
                this.grad3 = [[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],
                              [1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],
                              [0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]];
                this.p = [];
                for (let i = 0; i < 256; i++) {
                    this.p[i] = Math.floor(r.random() * 256);
                }
                this.perm = [];
                for (let i = 0; i < 512; i++) {
                    this.perm[i] = this.p[i & 255];
                }
            }

            dot(g, x, y, z) {
                return g[0] * x + g[1] * y + g[2] * z;
            }

            mix(a, b, t) {
                return (1.0 - t) * a + t * b;
            }

            fade(t) {
                return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
            }

            noise(x, y, z) {
                let X = Math.floor(x), Y = Math.floor(y), Z = Math.floor(z);
                x -= X; y -= Y; z -= Z;
                X &= 255; Y &= 255; Z &= 255;
                let gi000 = this.perm[X + this.perm[Y + this.perm[Z]]] % 12;
                let gi001 = this.perm[X + this.perm[Y + this.perm[Z + 1]]] % 12;
                let gi010 = this.perm[X + this.perm[Y + 1 + this.perm[Z]]] % 12;
                let gi011 = this.perm[X + this.perm[Y + 1 + this.perm[Z + 1]]] % 12;
                let gi100 = this.perm[X + 1 + this.perm[Y + this.perm[Z]]] % 12;
                let gi101 = this.perm[X + 1 + this.perm[Y + this.perm[Z + 1]]] % 12;
                let gi110 = this.perm[X + 1 + this.perm[Y + 1 + this.perm[Z]]] % 12;
                let gi111 = this.perm[X + 1 + this.perm[Y + 1 + this.perm[Z + 1]]] % 12;
                let n000 = this.dot(this.grad3[gi000], x, y, z);
                let n100 = this.dot(this.grad3[gi100], x - 1, y, z);
                let n010 = this.dot(this.grad3[gi010], x, y - 1, z);
                let n110 = this.dot(this.grad3[gi110], x - 1, y - 1, z);
                let n001 = this.dot(this.grad3[gi001], x, y, z - 1);
                let n101 = this.dot(this.grad3[gi101], x - 1, y, z - 1);
                let n011 = this.dot(this.grad3[gi011], x, y - 1, z - 1);
                let n111 = this.dot(this.grad3[gi111], x - 1, y - 1, z - 1);
                let u = this.fade(x), v = this.fade(y), w = this.fade(z);
                let nx00 = this.mix(n000, n100, u);
                let nx01 = this.mix(n001, n101, u);
                let nx10 = this.mix(n010, n110, u);
                let nx11 = this.mix(n011, n111, u);
                let nxy0 = this.mix(nx00, nx10, v);
                let nxy1 = this.mix(nx01, nx11, v);
                return this.mix(nxy0, nxy1, w);
            }
        }

        function lerp(start,end,t) {
            return start * (1 - t) + end * t;
        }

        const perlin = new ClassicalNoise();

        // Scene setup
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
        camera.position.set(-21.15, 60.31, -195.56); // Flying height
        camera.lookAt(0, 0, -100); // Look forward along -z
		//camera.rotation.set(-162.86, -5.90, -178.18);

        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        const gl = renderer.getContext();
        if (gl) {
            const ext = gl.getExtension('GL_EXT_tessellation_shader');
            if (ext) {
                gl.patchParameteri(gl.PATCH_VERTICES, 4); // for quads
            } else {
                console.error('Tessellation extension not supported');
            }
        }

        const controls = new OrbitControls(camera, renderer.domElement); // For debugging

		//const radian=180.0 / Math.PI;
		const radian=Math.PI / 180.0;
		function getDirection(theta, phi) { // dir = return
			let theta_left = theta + 90.0;
			let phi_up = phi - 90.0;
			if(theta_left>180.0) theta_left -=360.0;
			if(phi_up<-180.0) phi_up += 360.0;
		   
			//const x=Math.sin(90.0 * radian) * Math.cos(theta_left * radian);
			//const y=Math.cos(90.0 * radian);
			//const z=Math.sin(90.0 * radian) * Math.sin(theta_left * radian);
			
			const x=Math.sin(phi_up * radian) * Math.cos(theta * radian);
			const y=Math.cos(phi_up * radian);
			const z=Math.sin(phi_up * radian) * Math.sin(theta * radian);
			
			//const x=Math.sin(phi * radian) * Math.cos(theta * radian);
			//const y=Math.cos(phi);
			//const z=Math.sin(phi * radian) * Math.sin(theta * radian);

			//Vec3 XZonly={ // for player full directional speed not Y
			//    cos(theta * radian),
			//    0,
			//    sin(theta * radian)
			//};
			return new THREE.Vector3(x,y,z);
		}
		var sunTime=getDirection(90,170); // theta orient around horizontally, phi vertical/altitude
		// sunrise/sunset = phi at 180, above ground at 170, below ground at 190
		// front facing with camera at 90, back facing at -90 or 270
		// still need the light position itself


        // Lights (PBR-friendly)
        const ambientLight = new THREE.AmbientLight(0xffffff, 0.8); // Increased ambient for better brightness
        scene.add(ambientLight);
        const sunLight = new THREE.DirectionalLight(0xffffff, 1.5); // Increased intensity
        //sunLight.position.set(100, 100, 100); // Sun position for shadows/lighting (match sunDirection in skyMaterial)
		sunLight.position.set(sunTime.x*100,sunTime.y*100,sunTime.z*100); // Sun position from sphere coords
        scene.add(sunLight);

        //scene.fog = new THREE.FogExp2(0xaaccff, 0.00008); // color ≈ sky horizon, very low density
// Tune density: 0.00005 – 0.00015 depending on how fast you want fade
// Color should roughly match your sky bottomColor or horizon
        // Fog - tuned for visibility
        //scene.fog = new THREE.FogExp2(0xffffff, 0.0001); // Low density, match sky bottomColor
        scene.fog = new THREE.FogExp2(0xaaccff, 0.00008); // start low, increase to 0.00015–0.0003 to see effect faster

        const skyColors={
            topColor: [
                [80,124,255], // morning blue
                [57,206,255], // day cyan blue
                [16,4,255],   // evening deep blue
                [3,0,74],     // night dark blue
            ],
            bottomColor: [
                [183,255,245],// morning cyan white
                [198,212,255],// day blue white
                [186,89,255], // evening purple
                [12,3,175],   // night dim blue
            ],
        };
        // calculate the 86400 seconds to 4 segments time,
        // but not equally sized? day/night = 16hr~, morning/evening=8hr~
        // 86400/4 = 21600
        // 16/2 = 8 * 60 * 60 = 28800 / 2 = 14400 (4hr)
        //
        // morning= 1 to 14400 (+14400)
        // day= 14401 to 43200 (+28800)
        // evening= 43201 to 57600 (+14400)
        // night= 57601 to 86400 (+28800)
        //
        // cont in animate

        // Sky sphere (WoW-like gradient with sun)
        const skyGeometry = new THREE.SphereGeometry(5000, 32, 32);
        const skyMaterial = new THREE.ShaderMaterial({
            uniforms: {
                //topColor: { value: new THREE.Color(0x0077ff) }, // Blue sky
                //bottomColor: { value: new THREE.Color(0xffffff) }, // Horizon haze
				topColor: { value: new THREE.Color(0x6677ff) }, // Blue sky
                bottomColor: { value: new THREE.Color(0xffeeff) }, // Horizon haze
                sunColor: { value: new THREE.Color(0xfffffe) }, // Sun glow
                //sunDirection: { value: new THREE.Vector3(0.2, 0.2, 1).normalize() }, // Sun pos (normalized)
				sunDirection: { value: sunTime.normalize() }, // Sun pos (normalized) from sphere coords
                sunSize: { value: 0.003 }, // Sun disc size
            },
            vertexShader: `
                varying vec3 vWorldPosition;
                void main() {
                    vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
                    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
                }
            `,
            fragmentShader: `
                uniform vec3 topColor;
                uniform vec3 bottomColor;
                uniform vec3 sunColor;
                uniform vec3 sunDirection;
                uniform float sunSize;
                varying vec3 vWorldPosition;

                void main() {
                    vec3 viewDir = normalize(vWorldPosition - cameraPosition);
                    float height = normalize(vWorldPosition).y;
                    vec3 gradient = mix(bottomColor, topColor, max(height, 0.0)); // Gradient based on y
                    float sunDot = dot(viewDir, sunDirection);
                    float sunGlow = smoothstep(1.0 - sunSize, 1.0, sunDot); // Blended sun disc
                    gl_FragColor = vec4(mix(gradient, sunColor, sunGlow * 10.0), 1.0);
                }
            `,
            side: THREE.BackSide, // Inside out
            depthWrite: false,
			fog: false, // No fog on sky
        });
        const sky = new THREE.Mesh(skyGeometry, skyMaterial);
        scene.add(sky);

        // Terrain setup
        const tileSize = 4000; // Widened to hide edges easier
        const segments = 512; // Increased for better detail/resolution
        const heightScale = 50;
        const noiseScale = 100;
        const speed = 50; // Flying speed
        const numTiles = 5; // Keep this many tiles visible
        const terrainGroup = new THREE.Group();
        scene.add(terrainGroup);

        const textureLoader = new THREE.TextureLoader();
        // Load textures (replace paths; ensure they exist)
        const grassTex = textureLoader.load('./textures/grass.png');
        grassTex.wrapS = grassTex.wrapT = THREE.RepeatWrapping;
        grassTex.repeat.set(40, 40); // Increased repeat for larger tiles/better scaling
        const rockTex = textureLoader.load('./textures/rock.png');
        rockTex.wrapS = rockTex.wrapT = THREE.RepeatWrapping;
        rockTex.repeat.set(40, 40);
        const snowTex = textureLoader.load('./textures/snow.png');
        snowTex.wrapS = snowTex.wrapT = THREE.RepeatWrapping;
        snowTex.repeat.set(40, 40);
        const normalGrass = textureLoader.load('./textures/normal_grass.png');
        normalGrass.wrapS = normalGrass.wrapT = THREE.RepeatWrapping;
        normalGrass.repeat.set(40, 40);
        const normalRock = textureLoader.load('./textures/normal_rock.png');
        normalRock.wrapS = normalRock.wrapT = THREE.RepeatWrapping;
        normalRock.repeat.set(40, 40);
        const normalSnow = textureLoader.load('./textures/normal_snow.png');
        normalSnow.wrapS = normalSnow.wrapT = THREE.RepeatWrapping;
        normalSnow.repeat.set(40, 40);

        // GLSL Perlin noise
const glslPerlin = `
vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); }
float snoise(vec2 v){
  const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
  vec2 i  = floor(v + dot(v, C.yy) );
  vec2 x0 = v - i + dot(i, C.xx);
  vec2 i1;
  i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
  vec4 x12 = x0.xyxy + C.xxzz;
  x12.xy -= i1;
  i = mod(i, 289.0);
  vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) + i.x + vec3(0.0, i1.x, 1.0 ));
  vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
  m = m*m ;
  m = m*m ;
  vec3 x = 2.0 * fract(p * C.www) - 1.0;
  vec3 h = abs(x) - 0.5;
  vec3 ox = floor(x + 0.5);
  vec3 a0 = x - ox;
  m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
  vec3 g;
  g.x  = a0.x  * x0.x  + h.x  * x0.y;
  g.yz = a0.yz * x12.xz + h.yz * x12.yw;
  return 130.0 * dot(m, g);
}
`;

// Terrain material with tessellation
//const terrainMaterial = new THREE.ShaderMaterial({
const terrainMaterial = new THREE.RawShaderMaterial({
    //uniforms: THREE.UniformsUtils.merge([
    //    THREE.UniformsLib.lights,
    //    THREE.UniformsLib.fog,
    //    {
    uniforms: {
            grassTex: { value: grassTex },
            rockTex: { value: rockTex },
            snowTex: { value: snowTex },
            normalGrass: { value: normalGrass },
            normalRock: { value: normalRock },
            normalSnow: { value: normalSnow },
            repeatScale: { value: 40.0 },
            lightDir: { value: sunLight.position.normalize() },
            modelMatrix: { value: new THREE.Matrix4() },     // if needed in TCS/TES
            modelViewMatrix: { value: new THREE.Matrix4() },
            projectionMatrix: { value: new THREE.Matrix4() },
            normalMatrix: { value: new THREE.Matrix3() },
            // Fog uniforms (must match THREE.FogExp2)
            fogColor: { value: new THREE.Color(0xaaccff) },    // match your sky
            fogDensity: { value: 0.00008 },
            cameraPos: { value: camera.position }, // For distance in TCS
    },
    //    }
    //]),
    vertexShader: `
// Built-in inputs from geometry attributes (you MUST declare them yourself)
in vec3 position;
in vec2 uv;
in vec3 normal;

// Pass-through to TCS (these are the outs)
out vec3 vPosition;
out vec2 vUv;
out vec3 vNormal;

// Dummy outputs – just to satisfy fragment linker
// Use same name/type as in TES → fragment chain
out vec3 vWorldPos;
out float vHeight;
out float vFogDepth;

void main() {
    // Simply pass the built-in inputs forward
    vPosition = position;
    vUv       = uv;
    vNormal   = normal;

    // Dummy assignments – prevent unused varying warning + satisfy linker
    vWorldPos = vec3(0.0);     // arbitrary value, will be overwritten in TES
    vHeight   = 0.0;
    vFogDepth = 0.0;

    // No need to compute gl_Position here — TCS/TES will do it later
    // Some drivers require something, so minimal passthrough is ok:
    gl_Position = vec4(position, 1.0);  // dummy, will be ignored/overwritten
}
`,
    tessellationControlShader: `
#version 300 es

layout(vertices = 4) out;

in vec3 vPosition[];
in vec2 vUv[];
in vec3 vNormal[];

out vec3 tcPosition[];
out vec2 tcUv[];
out vec3 tcNormal[];

uniform vec3 cameraPos;  // ← your uniform

#define MAX_LEVEL 64.0
#define MIN_LEVEL 4.0

float getTessLevel(float dist) {
    return mix(MAX_LEVEL, MIN_LEVEL, smoothstep(500.0, 3000.0, dist));
}

void main() {
    // Copy inputs to outputs (now legal, since outs are writable)
    tcPosition[gl_InvocationID] = vPosition[gl_InvocationID];
    tcUv[gl_InvocationID]       = vUv[gl_InvocationID];
    tcNormal[gl_InvocationID]   = vNormal[gl_InvocationID];

    // Only compute tess levels once (invocation 0)
    if (gl_InvocationID == 0) {
        vec3 p0 = (modelMatrix * vec4(vPosition[0], 1.0)).xyz;
        vec3 p1 = (modelMatrix * vec4(vPosition[1], 1.0)).xyz;
        vec3 p2 = (modelMatrix * vec4(vPosition[2], 1.0)).xyz;
        vec3 p3 = (modelMatrix * vec4(vPosition[3], 1.0)).xyz;

        float d0 = distance(cameraPos, p0);
        float d1 = distance(cameraPos, p1);
        float d2 = distance(cameraPos, p2);
        float d3 = distance(cameraPos, p3);

        float e0 = getTessLevel((d1 + d2) * 0.5);
        float e1 = getTessLevel((d2 + d3) * 0.5);
        float e2 = getTessLevel((d3 + d0) * 0.5);
        float e3 = getTessLevel((d0 + d1) * 0.5);

        gl_TessLevelOuter[0] = e0;
        gl_TessLevelOuter[1] = e1;
        gl_TessLevelOuter[2] = e2;
        gl_TessLevelOuter[3] = e3;

        gl_TessLevelInner[0] = max(e1, e3);
        gl_TessLevelInner[1] = max(e0, e2);

        //--gl_TessLevelOuter[0] = 32.0;
        //--gl_TessLevelOuter[1] = 32.0;
        //--gl_TessLevelOuter[2] = 32.0;
        //--gl_TessLevelOuter[3] = 32.0;
        //--gl_TessLevelInner[0] = 32.0;
        //--gl_TessLevelInner[1] = 32.0;

        // Debug force high subdivision (uncomment to test)
        // gl_TessLevelOuter = vec4(32.0);
        // gl_TessLevelInner = vec2(32.0);
    }
}
    `,
    tessellationEvaluationShader: `
#version 300 es

layout(quads, fractional_even_spacing, ccw) in;

in vec3 tcPosition[];
in vec2 tcUv[];
in vec3 tcNormal[];

out vec2 vUv;
out float vHeight;
out vec3 vNormal;
out vec3 vWorldPos;
out float vFogDepth;

uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat3 normalMatrix;

// Ported ClassicalNoise to GLSL (exact match to your JS version)
struct ClassicalNoiseGLSL {
    ivec3[12] grad3;
    int[512] perm;
};

float dot(ivec3 g, vec3 xyz) {
    return float(g.x) * xyz.x + float(g.y) * xyz.y + float(g.z) * xyz.z;
}

float mix(float a, b, t) {
    return (1.0 - t) * a + t * b;
}

float fade(float t) {
    return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}

float noise(ClassicalNoiseGLSL cn, vec3 p) {
    vec3 P = p;
    ivec3 Pi = ivec3(floor(P));
    vec3 Pf = P - vec3(Pi);
    Pi = Pi & 255;

    int gi000 = cn.perm[Pi.x + cn.perm[Pi.y + cn.perm[Pi.z]] % 12;
    int gi001 = cn.perm[Pi.x + cn.perm[Pi.y + cn.perm[Pi.z + 1]] % 12;
    int gi010 = cn.perm[Pi.x + cn.perm[Pi.y + 1 + cn.perm[Pi.z]] % 12;
    int gi011 = cn.perm[Pi.x + cn.perm[Pi.y + 1 + cn.perm[Pi.z + 1]] % 12;
    int gi100 = cn.perm[Pi.x + 1 + cn.perm[Pi.y + cn.perm[Pi.z]] % 12;
    int gi101 = cn.perm[Pi.x + 1 + cn.perm[Pi.y + cn.perm[Pi.z + 1]] % 12;
    int gi110 = cn.perm[Pi.x + 1 + cn.perm[Pi.y + 1 + cn.perm[Pi.z]] % 12;
    int gi111 = cn.perm[Pi.x + 1 + cn.perm[Pi.y + 1 + cn.perm[Pi.z + 1]] % 12;

    float n000 = dot(cn.grad3[gi000], Pf - vec3(0.0,0.0,0.0));
    float n100 = dot(cn.grad3[gi100], Pf - vec3(1.0,0.0,0.0));
    float n010 = dot(cn.grad3[gi010], Pf - vec3(0.0,1.0,0.0));
    float n110 = dot(cn.grad3[gi110], Pf - vec3(1.0,1.0,0.0));
    float n001 = dot(cn.grad3[gi001], Pf - vec3(0.0,0.0,1.0));
    float n101 = dot(cn.grad3[gi101], Pf - vec3(1.0,0.0,1.0));
    float n011 = dot(cn.grad3[gi011], Pf - vec3(0.0,1.0,1.0));
    float n111 = dot(cn.grad3[gi111], Pf - vec3(1.0,1.0,1.0));

    vec3 fade_xyz = fade(Pf);
    float nx00 = mix(n000, n100, fade_xyz.x);
    float nx01 = mix(n001, n101, fade_xyz.x);
    float nx10 = mix(n010, n110, fade_xyz.x);
    float nx11 = mix(n011, n111, fade_xyz.x);
    float nxy0 = mix(nx00, nx10, fade_xyz.y);
    float nxy1 = mix(nx01, nx11, fade_xyz.y);
    return mix(nxy0, nxy1, fade_xyz.z);
}

ClassicalNoiseGLSL cn = ClassicalNoiseGLSL(); // Initialize in main or uniform if possible, but since static, define inside main

// In main, initialize grad3 and perm with your values (long, but copy from JS)
cn.grad3[0] = ivec3(1,1,0); cn.grad3[1] = ivec3(-1,1,0); // ... copy all 12
// cn.perm = int[512](0,1,2...); // copy the perm array from JS (it's fixed, so hardcode)

void main() {
    float u = gl_TessCoord.x;
    float v = gl_TessCoord.y;

    // Interpolation (same)
    vec3 pos = mix(mix(tcPosition[0], tcPosition[1], u), mix(tcPosition[3], tcPosition[2], u), v);

    vUv = mix(mix(tcUv[0], tcUv[1], u), mix(tcUv[3], tcUv[2], u), v) * 40.0;

    vNormal = normalize(normalMatrix * mix(mix(tcNormal[0], tcNormal[1], u), mix(tcNormal[3], tcNormal[2], u), v));

    // World pos for noise
    vec4 worldPos = modelMatrix * vec4(pos, 1.0);
    float noise = noise(cn, vec3(worldPos.x / 100.0, worldPos.z / 100.0, 0.0)); // your noiseScale=100
    pos.y += 500.0 * (noise + 0.5); // Boost to 500 for visible (test, then back to 50)

    // Debug sin (add if noise not visible)
    pos.y += 500.0 * sin(u * 2.0 * 3.14159 * 5.0) * sin(v * 2.0 * 3.14159 * 5.0); // Higher freq/amplitude

    vHeight = pos.y / 50.0; // Original heightScale

    vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
    vWorldPos = (modelMatrix * vec4(pos, 1.0)).xyz;
    vFogDepth = -mvPosition.z;

    gl_Position = projectionMatrix * mvPosition;

    // Approximate normal for lighting (after displacement)
    vec3 dx = dFdx(vWorldPos);
    vec3 dy = dFdy(vWorldPos);
    vNormal = normalize(cross(dx, dy));
}
    `,
    fragmentShader: `
precision highp float;

// Uniforms (declare all you use)
uniform sampler2D grassTex;
uniform sampler2D rockTex;
uniform sampler2D snowTex;
uniform sampler2D normalGrass;
uniform sampler2D normalRock;
uniform sampler2D normalSnow;
uniform float repeatScale;
uniform vec3 lightDir;
uniform vec3 cameraPos;  // ← your uniform

// Varyings from TES
in vec2 vUv;
in float vHeight;
in vec3 vNormal;
in vec3 vWorldPos;
in float vFogDepth;

// Fog uniforms (from THREE.UniformsLib.fog or manual)
uniform vec3 fogColor;
uniform float fogDensity;

// Output color (replaces gl_FragColor)
out vec4 fragColor;

void main() {
    // Texture lookups - use texture() not texture2D
    vec3 grassColor = texture(grassTex, vUv * repeatScale).rgb;
    vec3 rockColor  = texture(rockTex,  vUv * repeatScale).rgb;
    vec3 snowColor  = texture(snowTex,  vUv * repeatScale).rgb;

    vec3 albedo = mix(grassColor, rockColor, smoothstep(0.2, 0.5, vHeight));
    albedo = mix(albedo, snowColor, smoothstep(0.6, 0.8, vHeight));

    // Normals splatting
    vec3 normGrass = texture(normalGrass, vUv * repeatScale).rgb;
    vec3 normRock  = texture(normalRock,  vUv * repeatScale).rgb;
    vec3 normSnow  = texture(normalSnow,  vUv * repeatScale).rgb;

    vec3 norm = mix(normGrass, normRock, smoothstep(0.2, 0.5, vHeight));
    norm = mix(norm, normSnow, smoothstep(0.6, 0.8, vHeight));
    norm = norm * 2.0 - 1.0; // unpack [-1,1]

    // Hardcoded ORM values (as before)
    float ao = 1.0;
    float rough = 0.5;
    float metal = 0.0;

    // Lighting
    vec3 finalNormal = normalize(vNormal + norm * 0.5);
    float diff = max(dot(lightDir, finalNormal), 0.0) * ao;

    // Simple specular
    vec3 viewDir = normalize(cameraPos - vWorldPos);  // ← see uniform below
    vec3 halfway = normalize(lightDir + viewDir);
    float spec = pow(max(dot(finalNormal, halfway), 0.0), 32.0 * (1.0 - rough)) * (0.04 + metal);

    vec3 color = albedo * (diff + 0.3) + vec3(spec);

    // Fog application (using your vFogDepth)
    float fogFactor = 1.0 - exp(-fogDensity * fogDensity * vFogDepth * vFogDepth);
    color = mix(color, fogColor, fogFactor);

    fragColor = vec4(color, 1.0);
}
    `,
    glslVersion: THREE.GLSL3, // For #version 300 es
    side: THREE.DoubleSide,
    fog: true,
    lights: false // For directional light or false for full custom
});
terrainMaterial.wireframe = true; // uncomment temporarily

// For quad geometry
function createQuadGeometry(size, divs) {
    // "your" custom createQuadGeometry already generates the plane in the XZ plane (with y=0 for all vertices, x and z varying). This is different from PlaneGeometry, which defaults to the XY plane and requires the -Math.PI / 2 rotation to lay flat (grok x))
    const geometry = new THREE.BufferGeometry();
    const vertices = [];
    const uvs = [];
    const normals = [];
    const indices = [];

    const divSize = size / divs;
    for (let y = 0; y < divs; y++) {
        for (let x = 0; x < divs; x++) {
            const idx = vertices.length / 3;

            vertices.push(x * divSize - size / 2, 0, y * divSize - size / 2);
            uvs.push(x / divs, y / divs);
            normals.push(0, 1, 0);

            vertices.push((x + 1) * divSize - size / 2, 0, y * divSize - size / 2);
            uvs.push((x + 1) / divs, y / divs);
            normals.push(0, 1, 0);

            vertices.push((x + 1) * divSize - size / 2, 0, (y + 1) * divSize - size / 2);
            uvs.push((x + 1) / divs, (y + 1) / divs);
            normals.push(0, 1, 0);

            vertices.push(x * divSize - size / 2, 0, (y + 1) * divSize - size / 2);
            uvs.push(x / divs, (y + 1) / divs);
            normals.push(0, 1, 0);

            indices.push(idx, idx + 1, idx + 2, idx, idx + 2, idx + 3); // Triangles for quads, but for tess, we can use patches but Three.js will handle
        }
    }

    geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
    geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
    geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
    geometry.setIndex(indices);

    return geometry;
}

        // Function to get height at world position (using same noise)
        function getHeightAt(worldX, worldZ) {
            return heightScale * (perlin.noise(worldX / noiseScale, worldZ / noiseScale, 0) + 0.5);
        }

        function createTile(zOffset) {
            // negative z index start pos can cause offset difference on first 3 tiles compared to following 2 and all other if using worldpos for noise
            //console.log('createTile: '+zOffset);
            const baseDivs = 8;           // ← increased from 4 → better starting tessellation density
            const geometry = createQuadGeometry(tileSize, baseDivs);  // your custom quad grid function

            // Important: rotate the geometry to lay flat (XZ plane)
            //geometry.rotateX(-Math.PI / 2);

            const mesh = new THREE.Mesh(geometry, terrainMaterial);
            mesh.position.z = zOffset;

            // Optional debug: make it easier to see the base mesh
            //mesh.material.wireframe = true; // uncomment temporarily

            // No CPU height update needed anymore — displacement happens in TES
            return mesh;
        }
        
        function updateTileHeights(tile) {
            const geometry = tile.geometry;
            const vertices = geometry.attributes.position.array;
            for (let i = 0; i < vertices.length; i += 3) {
                const worldX = vertices[i] + tile.position.x;
                const worldZ = vertices[i + 2] + tile.position.z;
                vertices[i + 1] = getHeightAt(worldX, worldZ);
            }
            geometry.attributes.position.needsUpdate = true;
            geometry.computeVertexNormals();
        }

        // Function to add mountains to a tile (called on create and recycle)
        function addMountainsToTile(tile) {
            const mountainGeo = new THREE.ConeGeometry(20, 40, 32); // Larger mountains
            const mountainMat = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0 });

            for (let i = 0; i < 4; i++) { // Add a few per tile
                const posX = Math.random() * tileSize - tileSize / 2;
                const posZ = Math.random() * tileSize - tileSize / 2;
                const height = getHeightAt(posX + tile.position.x, posZ + tile.position.z);
                const mountain = new THREE.Mesh(mountainGeo, mountainMat);
                mountain.position.set(posX, height - 5, posZ); // Slightly below surface
                mountain.rotation.y = Math.random() * Math.PI * 2; // Random rotate
                tile.add(mountain);
            }

            // Example GLTF load and parent (async, replace with your model)
            // const loader = new GLTFLoader();
            // loader.load('./models/example.gltf', (gltf) => {
            //     const model = gltf.scene;
            //     const posX = Math.random() * tileSize - tileSize / 2;
            //     const posZ = Math.random() * tileSize - tileSize / 2;
            //     const height = getHeightAt(posX + tile.position.x, posZ + tile.position.z);
            //     model.position.set(posX, height, posZ);
            //     model.scale.set(5, 5, 5);
            //     tile.add(model);
            // });
        }

        // Initialize tiles
        const tiles = [];
        for (let i = 0; i < numTiles; i++) {
            const tile = createTile(i * tileSize - (numTiles / 2 * tileSize));
            addMountainsToTile(tile);
            tiles.push(tile);
            terrainGroup.add(tile);
        }

		var dd=null;
		var hh=0;
		var mm=0;
		var ss=0;
		var tsp=0;
		var ss_theta=0;
		var ss_phi=0;
		var testval=0;
        var tmpColor={};

        // Animation loop
        let lastTime = 0;
        const infoDiv = document.getElementById('info');
        function animate(time) {
            requestAnimationFrame(animate);
            const dt = (time - lastTime) / 1000;
            lastTime = time;
			
            // Move world toward camera
            terrainGroup.position.z -= speed * dt;

            // Recycle tiles
            const firstTile = tiles[0];
            if (firstTile.position.z + terrainGroup.position.z < -tileSize) {
                tiles.shift();
                firstTile.position.z += numTiles * tileSize;
                updateTileHeights(firstTile);
                while (firstTile.children.length) firstTile.remove(firstTile.children[0]);
                addMountainsToTile(firstTile);
                tiles.push(firstTile);
            }
            
            terrainMaterial.uniforms.cameraPos.value.copy(camera.position);
            // Optional – if you use them in TES/TCS
            terrainMaterial.uniforms.modelViewMatrix.value.multiplyMatrices(
                camera.matrixWorldInverse,
                terrainGroup.matrixWorld   // or per-tile if needed
            );
            terrainMaterial.uniforms.projectionMatrix.value.copy(camera.projectionMatrix);
            terrainMaterial.uniforms.normalMatrix.value.getNormalMatrix(terrainGroup.matrixWorld); // or per mesh

            // Update info box
            const pos = camera.position;
            const rot = camera.rotation;
            infoDiv.innerHTML = `
                Camera Position:<br>
                X: ${pos.x.toFixed(2)}<br>
                Y: ${pos.y.toFixed(2)}<br>
                Z: ${pos.z.toFixed(2)}<br><br>
                Camera Rotation:<br>
                X: ${(rot.x * 180 / Math.PI).toFixed(2)}°<br>
                Y: ${(rot.y * 180 / Math.PI).toFixed(2)}°<br>
                Z: ${(rot.z * 180 / Math.PI).toFixed(2)}°
            `;

            // sun position calculations
			dd=new Date();
			hh=dd.getHours()*60*60;
			mm=dd.getMinutes()*60;
			ss=dd.getSeconds()+mm+hh;
			//ss+=(6*60*60); // offset time, - 4-5 hours seem about accurate for sunset at 6pm front
            ss-=(12*60*60);
			if(ss>86400) ss-=86400;

			//testval++;
			//if(testval>2000) testval-=2000;
			//tsp=testval/1000; // debugtime
            tsp=ss/43200; // realtime

			ss_theta=(90);

			ss_phi=((tsp)*180)+90;

			sunTime=getDirection(ss_theta,ss_phi); // 90,170 = default front above ground, 180 phi = on ground
			//sunPhi should be 180 at 6am and 6pm, going to 300 at night and 60 on day
			//sunTheta should be 90 at 6pm, -90 or 270 at 6am
			
			sunLight.position.set(sunTime.x*1,sunTime.y*1,sunTime.z*1);
			skyMaterial.uniforms.sunDirection.value = sunTime.normalize();

            // sky color calculations
            var l=0; // segment selection
            //
            // realtime
            var t=ss; // range current
            var m=14400; // range max
            if(ss>57600) { // night
                l=3;
                t=ss-57600;
                m=28800;
            } else if(ss>43200) { // evening 
                l=2;
                t=ss-43200;
                m=14400;
            } else if(ss>14400) { // day
                l=1;
                t=ss-14400;
                m=28800;
            }
            //
            //debugtime
            //ss=testval;
            // morning
            //var t=ss; // range current
            //var m=200; // range max
            //if(ss>1200) { // night
            //    l=3;
            //    t=ss-1200;
            //    m=800;
            //} else if(ss>1000) { // evening 
            //    l=2;
            //    t=ss-1000;
            //    m=200;
            //} else if(ss>200) { // day
            //    l=1;
            //    t=ss-200;
            //    m=800;
            //}
            //
            //console.log("l="+l+" t="+t+" m="+m);
            var l2=l+1;
            if(l2>3) l2=0;

            tmpColor['r1']=skyColors['topColor'][l][0];
            tmpColor['g1']=skyColors['topColor'][l][1];
            tmpColor['b1']=skyColors['topColor'][l][2];
            tmpColor['r2']=skyColors['topColor'][l2][0];
            tmpColor['g2']=skyColors['topColor'][l2][1];
            tmpColor['b2']=skyColors['topColor'][l2][2];
            
            tmpColor['r3']=skyColors['bottomColor'][l][0];
            tmpColor['g3']=skyColors['bottomColor'][l][1];
            tmpColor['b3']=skyColors['bottomColor'][l][2];
            tmpColor['r4']=skyColors['bottomColor'][l2][0];
            tmpColor['g4']=skyColors['bottomColor'][l2][1];
            tmpColor['b4']=skyColors['bottomColor'][l2][2];

            tmpColor['rlt']=lerp(tmpColor['r1'],tmpColor['r2'],t/m);
            tmpColor['glt']=lerp(tmpColor['g1'],tmpColor['g2'],t/m);
            tmpColor['blt']=lerp(tmpColor['b1'],tmpColor['b2'],t/m);

            tmpColor['rlb']=lerp(tmpColor['r3'],tmpColor['r4'],t/m);
            tmpColor['glb']=lerp(tmpColor['g3'],tmpColor['g4'],t/m);
            tmpColor['blb']=lerp(tmpColor['b3'],tmpColor['b4'],t/m);
		    
            skyMaterial.uniforms.topColor.value=new THREE.Color(tmpColor['rlt']/255,tmpColor['glt']/255,tmpColor['blt']/255);
            skyMaterial.uniforms.bottomColor.value=new THREE.Color(tmpColor['rlb']/255,tmpColor['glb']/255,tmpColor['blb']/255);
            // sun and sky calculations end


            controls.update();
            renderer.render(scene, camera);
        }
        animate(0);

        // Resize handler
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });
    </script>
</body>
</html>

Top
©twily.info 2013 - 2026
twily at twily dot info



2 550 795 visits
... ^ v