Skip welcome & menu and move to editor
Welcome to JS Bin
Load cached copy from
 
<!DOCTYPE html>
<html lang="en">
<head>
    <title>3D Terrain with custom layer</title>
    <meta charset='utf-8'>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel='stylesheet' href='https://unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.css' />
    <script src='https://unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.js'></script>
    <style>
        body { margin: 0; padding: 0; }
        html, body, #map { height: 100%; }
    </style>
</head>
<body>
<div id="map"></div>
<script>
const EXTENT = 8192;
const TILE_SIZE = 256;
const map = new maplibregl.Map({
    container: 'map',
    style: {
        version: 8,
        glyphs: "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
        sources: {
            osm: {
                type: 'raster',
                tileSize: 256,
                tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
                attribution: '© OpenStreetMap Contributors',
                maxzoom: 19
            },
            terrain: {
                type: 'raster-dem',
                tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'],
                tileSize: 256,
                maxzoom: 15,
                encoding: 'terrarium'
            }
        },
        layers: [
            {
                id: 'osm',
                type: 'raster',
                source: 'osm',
                paint: {
                    'raster-opacity': 1.0
                }
            }
        ],
        terrain: {
            source: 'terrain',
            exaggeration: 1.0
        }
    },
    zoom: 12,
    center: [5.7245, 45.1885],
    pitch: 60,
    bearing: 0
});
const terrainNormalLayer = {
    id: 'terrain-normal',
    type: 'custom',
    renderingMode: '3d',
    shaderMap: new Map(),
    onAdd(map, gl) {
        this.map = map;
        this.gl = gl;
    },
    
    getShader(gl, shaderDescription) {
        if (this.shaderMap.has(shaderDescription.variantName)) {
            return this.shaderMap.get(shaderDescription.variantName);
        }
        const vertexSource = `#version 300 es
            ${shaderDescription.vertexShaderPrelude}
            ${shaderDescription.define}
            uniform sampler2D u_image;
            uniform vec2 u_dimension;
            uniform int u_original_vertex_count;
            
            in vec2 a_pos;
            out vec2 v_pos;
            out vec3 v_normal;
            out float v_elevation;
            float getElevation(vec2 pos) {
                vec2 texCoord = pos / float(${EXTENT});
                vec4 data = texture(u_image, texCoord) * 255.0;
                return (data.r * 256.0 + data.g + data.b / 256.0) - 32768.0;
            }
            void main() {
                vec2 pos = a_pos;
                float elevation = getElevation(pos);
                v_elevation = elevation;
                v_pos = pos / float(${EXTENT});
                bool isBorder = gl_VertexID >= u_original_vertex_count;
                if (isBorder) {
                    elevation -= 200.0;
                }
                gl_Position = projectTileFor3D(pos, elevation);
                
                // Simple normal calculation
                vec2 epsilon = vec2(1.0) / u_dimension;
                float nx = getElevation(pos + vec2(epsilon.x, 0.0)) - getElevation(pos - vec2(epsilon.x, 0.0));
                float ny = getElevation(pos + vec2(0.0, epsilon.y)) - getElevation(pos - vec2(0.0, epsilon.y));
                v_normal = normalize(vec3(-nx, -ny, 2.0 * length(epsilon)));
            }`;
        const fragmentSource = `#version 300 es
            precision highp float;
            
            in vec2 v_pos;
            in vec3 v_normal;
            in float v_elevation;
            out vec4 fragColor;
            void main() {
                vec2 distFromEdge = min(v_pos, 1.0 - v_pos);
                float borderWidth = 20.0 / float(${EXTENT});
                
                if (distFromEdge.x < borderWidth || distFromEdge.y < borderWidth) {
                    fragColor = vec4(1.0, 0.0, 0.0, 1.0);  // Red borders
                } else {
                    float normalizedElevation = (v_elevation + 500.0) / 1500.0;
                    normalizedElevation = clamp(normalizedElevation, 0.0, 1.0);
                    vec3 color = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), normalizedElevation);
                    fragColor = vec4(color, 0.9);
                }
            }`;
        const program = gl.createProgram();
        const vertexShader = gl.createShader(gl.VERTEX_SHADER);
        const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(vertexShader, vertexSource);
        gl.compileShader(vertexShader);
        if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
            console.error('Vertex shader compilation failed:', gl.getShaderInfoLog(vertexShader));
            return null;
        }
        gl.shaderSource(fragmentShader, fragmentSource);
        gl.compileShader(fragmentShader);
        if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
            console.error('Fragment shader compilation failed:', gl.getShaderInfoLog(fragmentShader));
            return null;
        }
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);
        if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
            console.error('Program link failed:', gl.getProgramInfoLog(program));
            return null;
        }
        const uniforms = [
            'u_matrix',
            'u_projection_matrix',
            'u_projection_clipping_plane',
            'u_projection_transition',
            'u_projection_tile_mercator_coords',
            'u_projection_fallback_matrix',
            'u_image',
            'u_dimension',
            'u_original_vertex_count'
        ];
        const locations = {};
        for (const uniform of uniforms) {
            locations[uniform] = gl.getUniformLocation(program, uniform);
        }
        const attributes = {
            a_pos: gl.getAttribLocation(program, 'a_pos')
        };
        const result = { program, locations, attributes };
        this.shaderMap.set(shaderDescription.variantName, result);
        return result;
    },
    getTileMesh(gl) {
        const meshBuffers = maplibregl.createTileMesh({
            granularity: 128,
        }, '16bit');
        const vertices = new Int16Array(meshBuffers.vertices);
        const indices = new Uint16Array(meshBuffers.indices);
        const vbo = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
        gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
        const ibo = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
        return {
            vbo,
            ibo,
            indexCount: indices.length,
            originalVertexCount: vertices.length / 2
        };
    },
    
    render(gl, matrix) {
        const terrain = this.map.terrain;
        if (!terrain) return;
        const shader = this.getShader(gl, matrix.shaderData);
        gl.useProgram(shader.program);
        const renderableTiles = terrain.sourceCache.getRenderableTiles();
        gl.disable(gl.DEPTH_TEST);
        gl.depthFunc(gl.LEQUAL);
        gl.enable(gl.BLEND);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
        // Create mesh once
        const mesh = this.getTileMesh(gl);
        gl.bindBuffer(gl.ARRAY_BUFFER, mesh.vbo);
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, mesh.ibo);
        gl.enableVertexAttribArray(shader.attributes.a_pos);
        gl.vertexAttribPointer(
            shader.attributes.a_pos,
            2,
            gl.SHORT,
            false,
            4,
            0
        );
        const renderedCanonical = new Set();
        for (const tile of renderableTiles) {
            const canonical = tile.tileID.canonical;
            const key = `${canonical.z}/${canonical.x}/${canonical.y}`;
            if (renderedCanonical.has(key)) continue;
            renderedCanonical.add(key);
            const terrainData = terrain.getTerrainData(tile.tileID);
            if (!terrainData || !terrainData.texture) continue;
            gl.activeTexture(gl.TEXTURE0);
            gl.bindTexture(gl.TEXTURE_2D, terrainData.texture);
            gl.uniform1i(shader.locations.u_image, 0);
            const projectionData = this.map.transform.getProjectionData({
                overscaledTileID: tile.tileID,
                applyGlobeMatrix: true
            });
            gl.uniform4f(shader.locations.u_projection_clipping_plane, ...projectionData.clippingPlane);
            gl.uniform1f(shader.locations.u_projection_transition, projectionData.projectionTransition);
            gl.uniform4f(shader.locations.u_projection_tile_mercator_coords, ...projectionData.tileMercatorCoords);
            gl.uniformMatrix4fv(shader.locations.u_projection_matrix, false, projectionData.mainMatrix);
            gl.uniformMatrix4fv(shader.locations.u_projection_fallback_matrix, false, projectionData.fallbackMatrix);
            
            gl.uniform2f(shader.locations.u_dimension, 256, 256);
            gl.uniform1i(shader.locations.u_original_vertex_count, mesh.originalVertexCount);
            gl.drawElements(gl.TRIANGLES, mesh.indexCount, gl.UNSIGNED_SHORT, 0);
        }
        gl.disable(gl.DEPTH_TEST);
        gl.disable(gl.BLEND);
    }
};
map.on('load', () => {
    console.log('Map loaded');
    map.setTerrain({source: 'terrain', exaggeration: 1.0});
    console.log('Terrain set');
    map.addLayer(terrainNormalLayer);
    console.log('Layer added');
        const terrainSource = map.getSource('terrain');
    console.log('Terrain source state:', {
        loaded: terrainSource._loaded,
        hasTiles: !!terrainSource.tiles,
        tileSize: terrainSource.tileSize
    });
});
map.addControl(new maplibregl.NavigationControl());
</script>
</body>
</html>
Output 300px

You can jump to the latest bin by adding /latest to your URL

Dismiss x
public
Bin info
X-plor3rpro
0viewers