I am rendering a plane made of many triangles in WebGL 2 using Three.js, and I want to offset the vertices in the plane according to a Perlin noise function. However, my noise function seems to work in the fragment shader, but not in the vertex shader.
vertex shader:
#version 300 es
precision highp float;
vec2 rand2d (vec2 uv_0) {
return vec2(
fract(sin(dot(uv_0, vec2(9832., -8933.2))) * 1938.4), fract(cos(dot(uv_0, vec2(-5294.2, 1243.2))) * 9043.)) * 2. - 1.;
}
float perlin_noise (vec2 uv_1) {
vec2 v00 = floor(uv_1);
vec2 v10 = v00 + vec2(1., 0.), v01 = v00 + vec2(0., 1.), v11 = v00 + vec2(1., 1.);
vec2 vxy = fract(uv_1);
float i00 = dot(rand2d(v00), vxy), i01 = dot(rand2d(v01), vxy - vec2(0., 1.)), i10 = dot(rand2d(v10), vxy - vec2(1., 0.)), i11 = dot(rand2d(v11), vxy - vec2(1., 1.));
vec2 s = smoothstep(0.0, 1.0, vxy);
return mix(mix(i00, i10, s.x), mix(i01, i11, s.x), s.y);
}
// supplied by three
in vec3 position;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform vec3 cameraPosition;
// supplied by me
uniform float spacing;
uniform vec2 offset;
uniform vec2 size;
out vec3 offsetPosition;
void main() {
// vec3 offsetPosition = position;
offsetPosition = position + vec3(offset.x, 0, offset.y);
offsetPosition.z = floor((cameraPosition.z + offsetPosition.z) / spacing) * spacing;
offsetPosition.y = perlin_noise(offsetPosition.xz / 10.) * 10.;
gl_Position = projectionMatrix * modelViewMatrix * vec4(offsetPosition, 1.0);
}
fragment shader:
#version 300 es
precision highp float;
vec2 rand2d (vec2 uv_0) {
return vec2(
fract(sin(dot(uv_0, vec2(9832., -8933.2))) * 1938.4), fract(cos(dot(uv_0, vec2(-5294.2, 1243.2))) * 9043.)) * 2. - 1.;
}
float perlin_noise (vec2 uv_1) {
vec2 v00 = floor(uv_1);
vec2 v10 = v00 + vec2(1., 0.), v01 = v00 + vec2(0., 1.), v11 = v00 + vec2(1., 1.);
vec2 vxy = fract(uv_1);
float i00 = dot(rand2d(v00), vxy), i01 = dot(rand2d(v01), vxy - vec2(0., 1.)), i10 = dot(rand2d(v10), vxy - vec2(1., 0.)), i11 = dot(rand2d(v11), vxy - vec2(1., 1.));
vec2 s = smoothstep(0.0, 1.0, vxy);
return mix(mix(i00, i10, s.x), mix(i01, i11, s.x), s.y);
}
in vec3 offsetPosition;
out highp vec4 fragColor;
void main() {
float value = perlin_noise(offsetPosition.xz / 10.) * .5 + .5;
fragColor = vec4(value, offsetPosition.y, 0., 1.);
}
result:
The plane is completely flat (there are lots of triangles in that image, I swear), and appears red instead of yellow/green, which suggests that offsetPosition.y
is being set to zero, which means that my Perlin noise function, which is identical in the vertex shader and the fragment shader, is returning 0 in the vertex shader only. Why is this happening?
The issue is, that you've to less triangles, respectively vertices. The vertices of your grid are distributed in that way, that the result of offsetPosition.xz / 10.0
are integral coordinates (no fractional component). The perlin noise is a periodic function with period length of 1.0. So for all integral coordinates, the result is same.
Change to
offsetPosition.y = perlin_noise(offsetPosition.xz / 100.0) * 10.0;
and you will get different heights for the vertices.
In general the algorithm works fine. See the example where I've used a 100x100 grid with 100x100 tiles. So offsetPosition.xz / 10.0
generates coordinates with steps of tenth:
(function onLoad() {
var camera, scene, renderer, orbitControls;
init();
animate();
function init() {
let canvas = document.createElement( 'canvas' );
let context = canvas.getContext( 'webgl2', { alpha: false } );
renderer = new THREE.WebGLRenderer( { canvas: canvas, context: context, antialias: true, alpha: true } );
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 100);
camera.position.set(0, 10, -40);
scene = new THREE.Scene();
scene.background = new THREE.Color(0);
scene.add(camera);
window.onresize = resize;
orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
addGridHelper();
createModel();
}
function createModel() {
var uniforms = {
spacing : {type:'f', value: 0.0001}
};
var material = new THREE.ShaderMaterial({
side: THREE.DoubleSide,
uniforms: uniforms,
vertexShader: document.getElementById('vertex-shader').textContent,
fragmentShader: document.getElementById('fragment-shader').textContent,
});
var geometry = new THREE.PlaneBufferGeometry( 100, 100, 100, 100);
geometry.rotateX(Math.PI/2.0);
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
}
function addGridHelper() {
var helper = new THREE.GridHelper(100, 100);
helper.material.opacity = 1.0;
helper.material.transparent = true;
scene.add(helper);
var axis = new THREE.AxesHelper(1000);
scene.add(axis);
}
function resize() {
var aspect = window.innerWidth / window.innerHeight;
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = aspect;
camera.updateProjectionMatrix();
}
function animate() {
requestAnimationFrame(animate);
orbitControls.update();
render();
}
function render() {
renderer.render(scene, camera);
}
})();
<script type='x-shader/x-vertex' id='vertex-shader'>
#version 300 es
precision highp float;
vec2 rand2d (vec2 uv_0) {
return vec2(
fract(sin(dot(uv_0, vec2(9832., -8933.2))) * 1938.4), fract(cos(dot(uv_0, vec2(-5294.2, 1243.2))) * 9043.)) * 2. - 1.;
}
float perlin_noise (vec2 uv_1) {
vec2 v00 = floor(uv_1);
vec2 v10 = v00 + vec2(1., 0.), v01 = v00 + vec2(0., 1.), v11 = v00 + vec2(1., 1.);
vec2 vxy = fract(uv_1);
float i00 = dot(rand2d(v00), vxy), i01 = dot(rand2d(v01), vxy - vec2(0., 1.)), i10 = dot(rand2d(v10), vxy - vec2(1., 0.)), i11 = dot(rand2d(v11), vxy - vec2(1., 1.));
vec2 s = smoothstep(0.0, 1.0, vxy);
return mix(mix(i00, i10, s.x), mix(i01, i11, s.x), s.y);
}
// supplied by me
uniform float spacing;
uniform vec2 offset;
uniform vec2 size;
out vec3 offsetPosition;
void main() {
offsetPosition = position + vec3(offset.x, 0, offset.y);
offsetPosition.y = perlin_noise(offsetPosition.xz / 10.0) * 10.0;
offsetPosition.z = floor(offsetPosition.z / spacing) * spacing;
gl_Position = projectionMatrix * modelViewMatrix * vec4(offsetPosition, 1.0);
}
</script>
<script type='x-shader/x-fragment' id='fragment-shader'>
#version 300 es
precision highp float;
vec2 rand2d (vec2 uv_0) {
return vec2(
fract(sin(dot(uv_0, vec2(9832., -8933.2))) * 1938.4), fract(cos(dot(uv_0, vec2(-5294.2, 1243.2))) * 9043.)) * 2. - 1.;
}
float perlin_noise (vec2 uv_1) {
vec2 v00 = floor(uv_1);
vec2 v10 = v00 + vec2(1., 0.), v01 = v00 + vec2(0., 1.), v11 = v00 + vec2(1., 1.);
vec2 vxy = fract(uv_1);
float i00 = dot(rand2d(v00), vxy), i01 = dot(rand2d(v01), vxy - vec2(0., 1.)), i10 = dot(rand2d(v10), vxy - vec2(1., 0.)), i11 = dot(rand2d(v11), vxy - vec2(1., 1.));
vec2 s = smoothstep(0.0, 1.0, vxy);
return mix(mix(i00, i10, s.x), mix(i01, i11, s.x), s.y);
}
in vec3 offsetPosition;
out highp vec4 fragColor;
void main() {
float value = perlin_noise(offsetPosition.xz / 10.0) * .5 + .5;
fragColor = vec4(value, offsetPosition.y * 0.05 + 0.5, 0., 1.);
}
</script>
<!--script src="https://threejs.org/build/three.min.js"></script-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>