Three.js: Sampling from heightmap with Points - javascript

I'm trying to sample from a heightmap to position the z coordinate of some points but am evidently making an elementary mistake. Here's my scene:
// generate a scene object
var scene = new THREE.Scene();
// generate a camera
var aspectRatio = window.innerWidth / window.innerHeight;
var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.001, 10);
// generate a renderer
var renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
renderer.setPixelRatio(window.devicePixelRatio); // <3 retina
renderer.setSize(window.innerWidth, window.innerHeight); // canvas size
document.body.appendChild(renderer.domElement);
// generate some lights
var ambientLight = new THREE.AmbientLight(0xeeeeee);
scene.add(ambientLight);
// generate controls
var controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.zoomSpeed = 0.4;
controls.panSpeed = 0.2;
// render loop
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
controls.update();
};
/**
* Heightmap canvas
**/
function getHeightmap(cb) {
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
image = new Image();
image.crossOrigin = 'Anonymous';
image.onload = function(img) {
ctx.drawImage(this, 0, 0, this.width, this.height);
cb(ctx.getImageData(0,0, this.height, this.width));
}
image.src = 'https://s3.amazonaws.com/duhaime/blog/visualizations/word-to-viz/heightmap.jpg';
}
/**
* Geometry
**/
function addLetters(data) {
window.data = data;
var n = 100000, // num observations
translations = new Float32Array(n*3),
iter = 0,
inf = Number.POSITIVE_INFINITY,
ninf = Number.NEGATIVE_INFINITY
domains = {x: {min: inf, max: ninf}, y: {min: inf, max: ninf}};
// set the data domains
var domains = {x: {min: 0, max: 10,}, y: {min: 0, max: 10,}};
for (var i=0; i<n; i++) {
// unscaled coords
var x = Math.random() * 10,
y = Math.random() * 10;
// scale x, y, and z 0:1
var x = (x-domains.x.min)/(domains.x.max-domains.x.min),
y = (y-domains.y.min)/(domains.y.max-domains.y.min);
// validate x and y dims in bounds
if (x > 1 || x < 0) console.error(x)
if (y > 1 || y < 0) console.error(y)
// sample from the heightmap at the point's location
var row = Math.floor(y * data.height),
col = Math.floor(x * data.width),
idx = (row * 4) + (col * 4),
z = data.data[idx];
translations[iter++] = x;
translations[iter++] = y;
translations[iter++] = (z/255) * 0.1;
}
// center the camera
camera.position.set(0.5, 0.5, 1);
controls.target.set(0.5, 0.2, -2);
controls.update();
var geometry = new THREE.InstancedBufferGeometry();
geometry.addAttribute('position',
new THREE.BufferAttribute(new Float32Array([0, 0, 0]), 3, 1));
geometry.addAttribute('translation',
new THREE.InstancedBufferAttribute(translations, 3, true, 1) );
var material = new THREE.RawShaderMaterial({
vertexShader: document.getElementById('vertex-shader').textContent,
fragmentShader: document.getElementById('fragment-shader').textContent,
uniforms: {
pointScale: {
type: 'f',
value: window.devicePixelRatio * window.innerHeight * 0.005,
}
}
});
material.transparent = true;
var mesh = new THREE.Points(geometry, material);
mesh.frustumCulled = false; // prevent mesh clipped on drag
mesh.rotation.x = -Math.PI * 0.35; // tilt the mesh back away from cam
scene.add(mesh);
}
/**
* Helpers
**/
// load heightmap data and render
getHeightmap(function(data) {
addLetters(data);
render();
})
html,
body {
width: 100%;
height: 100%;
}
body {
margin: 0;
overflow: hidden;
background: linear-gradient(#585852, #262726);
}
#letter-canvas {
position: absolute;
top: 0;
left: 0;
}
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/97/three.min.js'></script>
<script src='https://s3.amazonaws.com/duhaime/blog/visualizations/word-to-viz/trackball-controls.min.js'></script>
<script id='vertex-shader' type='x-shader/x-vertex'>
//uniform sampler2D map; // character map
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform vec3 cameraPosition;
uniform float pointScale;
attribute vec3 position;
attribute vec3 translation;
varying float vOpacity;
float scalePointZ(in vec4 pos, in vec3 cameraPosition) {
float zDelta = pow(pos[2] - cameraPosition[2], 2.0);
return pointScale / pow(zDelta, 0.5);
}
void main() {
// project this particle
vec3 raw = position + translation;
vec4 world = modelViewMatrix * vec4(raw, 1.0);
gl_Position = projectionMatrix * world;
// set the size of each particle
gl_PointSize = scalePointZ(world, cameraPosition);
vOpacity = clamp(cameraPosition.z - world.z, 0.0, 1.0);
}
</script>
<script id='fragment-shader' type='x-shader/x-fragment'>
precision mediump float;
varying float vOpacity;
void main() {
// make point circular
vec2 coord = gl_PointCoord - vec2(0.5);
if (length(coord) > 0.5) discard;
// color the point
gl_FragColor = vec4(0.7, 0.7, 0.8, vOpacity);
}
</script>
As one can see, the heightmap is setting the z position of each point, but there are striations in the z coordinates that are not present in the heightmap, so I must be sampling from it incorrectly.
Can anyone see why that might be the case? Any insights would be very helpful!

The formula to calculate the index of a pixel in the 2 dimensional height map image is
idx = (row * 4 * data.width) + (col * 4)
rather than
idx = (row * 4) + (col * 4)
When the image is loaded to the data array, the the size of the canvas has to be suited to the size of the height map image:
function getHeightmap(cb) {
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
image = new Image();
image.crossOrigin = 'Anonymous';
image.onload = function(img) {
canvas.width = this.width; // <----- set width and height of the canvas
canvas.height = this.height;
ctx.drawImage(this, 0, 0, this.width, this.height);
cb(ctx.getImageData(0,0, this.width, this.height));
}
image.src = 'https://s3.amazonaws.com/duhaime/blog/visualizations/word-to-viz/heightmap.jpg';
}
See the example. I applied the suggestions to your code and I tint the height map from blue to red:
// generate a scene object
var scene = new THREE.Scene();
// generate a camera
var aspectRatio = window.innerWidth / window.innerHeight;
var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.001, 10);
// generate a renderer
var renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
renderer.setPixelRatio(window.devicePixelRatio); // <3 retina
renderer.setSize(window.innerWidth, window.innerHeight); // canvas size
document.body.appendChild(renderer.domElement);
// generate some lights
var ambientLight = new THREE.AmbientLight(0xeeeeee);
scene.add(ambientLight);
// generate controls
var controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.zoomSpeed = 0.4;
controls.panSpeed = 0.2;
window.onresize = function() {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
// render loop
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
controls.update();
};
/**
* Heightmap canvas
**/
function getHeightmap(cb) {
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
image = new Image();
image.crossOrigin = 'Anonymous';
image.onload = function(img) {
canvas.width = this.width; // <----- set widht and height of the canvas
canvas.height = this.height;
ctx.drawImage(this, 0, 0, this.width, this.height);
cb(ctx.getImageData(0,0, this.width, this.height));
}
image.src = 'https://s3.amazonaws.com/duhaime/blog/visualizations/word-to-viz/heightmap.jpg';
}
/**
* Geometry
**/
function addLetters(data) {
window.data = data;
var n = 100000, // num observations
translations = new Float32Array(n*3),
iter = 0,
inf = Number.POSITIVE_INFINITY,
ninf = Number.NEGATIVE_INFINITY
domains = {x: {min: inf, max: ninf}, y: {min: inf, max: ninf}};
// set the data domains
var domains = {x: {min: 0, max: 10,}, y: {min: 0, max: 10,}};
let min_x = 0, max_x = 0, min_y = 0, max_y = 0;
for (var i=0; i<n; i++) {
// unscaled coords
var x = Math.random() * 10,
y = Math.random() * 10;
// scale x, y, and z 0:1
var x = (x-domains.x.min)/(domains.x.max-domains.x.min),
y = (y-domains.y.min)/(domains.y.max-domains.y.min);
// validate x and y dims in bounds
if (x > 1 || x < 0) console.error(x)
if (y > 1 || y < 0) console.error(y)
// sample from the heightmap at the point's location
var row = Math.floor(y * data.height),
col = Math.floor(x * data.width),
idx = (row * 4 * data.width) + (col * 4),
z = data.data[idx];
translations[iter++] = x;
translations[iter++] = y;
translations[iter++] = (z/255) * 0.1;
}
// center the camera
camera.position.set(0.5, 0.5, 1);
controls.target.set(0.5, 0.2, -2);
controls.update();
var geometry = new THREE.InstancedBufferGeometry();
geometry.addAttribute('position',
new THREE.BufferAttribute(new Float32Array([0, 0, 0]), 3, 1));
geometry.addAttribute('translation',
new THREE.InstancedBufferAttribute(translations, 3, true, 1) );
var material = new THREE.RawShaderMaterial({
vertexShader: document.getElementById('vertex-shader').textContent,
fragmentShader: document.getElementById('fragment-shader').textContent,
uniforms: {
pointScale: {
type: 'f',
value: window.devicePixelRatio * window.innerHeight * 0.005,
}
}
});
material.transparent = true;
var mesh = new THREE.Points(geometry, material);
mesh.frustumCulled = false; // prevent mesh clipped on drag
mesh.rotation.x = -Math.PI * 0.35; // tilt the mesh back away from cam
scene.add(mesh);
}
/**
* Helpers
**/
// load heightmap data and render
getHeightmap(function(data) {
addLetters(data);
render();
})
html,
body { width: 100%; height: 100%; }
body { margin: 0; overflow: hidden; background: linear-gradient(#585852, #262726); }
#letter-canvas { position: absolute; top: 0; left: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/103/three.min.js"></script>
<script src='https://s3.amazonaws.com/duhaime/blog/visualizations/word-to-viz/trackball-controls.min.js'></script>
<script id='vertex-shader' type='x-shader/x-vertex'>
//uniform sampler2D map; // character map
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform vec3 cameraPosition;
uniform float pointScale;
attribute vec3 position;
attribute vec3 translation;
varying float vOpacity;
float scalePointZ(in vec4 pos, in vec3 cameraPosition) {
float zDelta = pow(pos[2] - cameraPosition[2], 2.0);
return pointScale / pow(zDelta, 0.5);
}
void main() {
// project this particle
vec3 raw = position + translation;
vec4 world = modelViewMatrix * vec4(raw, 1.0);
gl_Position = projectionMatrix * world;
// set the size of each particle
gl_PointSize = scalePointZ(world, cameraPosition);
vOpacity = clamp(translation.z*10.0, 0.0, 1.0);
}
</script>
<script id='fragment-shader' type='x-shader/x-fragment'>
precision mediump float;
varying float vOpacity;
void main() {
// make point circular
vec2 coord = gl_PointCoord - vec2(0.5);
if (length(coord) > 0.5) discard;
// color the point
gl_FragColor = vec4(vOpacity, 0.3, 1.0 - vOpacity, vOpacity*0.5+0.5);
}
</script>

Related

WebGL Renders pixelated lines

Im trying to render simple shapes ( circles, rectangles and triangles , however, they become very pixelated when WebGL Renders them.
Shader code:
<!-- vertex shader -->
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
uniform vec2 u_resolution;
void main() {
// convert the rectangle points from pixels to 0.0 to 1.0
vec2 zeroToOne = a_position / u_resolution;
// convert from 0->1 to 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// convert from 0->2 to -1->+1 (clipspace)
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
}
</script>
<!-- fragment shader -->
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
</script>
Here is my code for rendering the circle:
var WebGLRenderer = (function () {
function WebGLRenderer() {
this.canvas = document.getElementById('canvas')
this.gl = this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl')
if (!this.gl) {
throw Error('Your browser does not support WebGL')
return
}
// Programs
this.rectangleProgram = webglUtils.createProgramFromScripts(this.gl, ['2d-vertex-shader', '2d-fragment-shader'])
// Locations
this.rectanglePoisitionLocation = this.gl.getAttribLocation(this.rectangleProgram, 'a_position')
// Uniforms
this.rectangleResolutionLocation = this.gl.getUniformLocation(this.rectangleProgram, 'u_resolution')
this.rectangleColorLocation = this.gl.getUniformLocation(this.rectangleProgram, 'u_color')
// this.positionBuffer = this.gl.createBuffer()
this.rectanglePositionBuffer = this.gl.createBuffer()
// this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer)
requestAnimationFrame(this.render.bind(this))
}
WebGLRenderer.prototype.clearCanvas = function (color) {
var rgba = color.getColor()
this.gl.clearColor(...rgba)
this.gl.clear(this.gl.COLOR_BUFFER_BIT)
}
WebGLRenderer.prototype.drawCircle = function (x, y, radius, color) {
// Render circle
// For now user rectangleProgram
this.gl.useProgram(this.rectangleProgram)
this.gl.enableVertexAttribArray(this.rectanglePoisitionLocation)
// this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer)
this.circleBuffer = this.gl.createBuffer()
// this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.rectanglePositionBuffer)
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.circleBuffer)
// Setup circle
var circleVertices = [x, y]
var numFans = 360
var anglePerFan = (2 * Math.PI) / numFans
for (var i = 0; i <= numFans; i++) {
var angle = anglePerFan * (i + 1)
var angledX = x + Math.cos(angle) * radius
var angledY = y + Math.sin(angle) * radius
circleVertices.push(angledX, angledY)
// circleVertices.push()
}
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(circleVertices), this.gl.DYNAMIC_DRAW)
// this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW)
var size = 2
var type = this.gl.FLOAT
var normalize = false
var stride = 0
var offset = 0
this.gl.vertexAttribPointer(this.rectanglePoisitionLocation, size, type, normalize, stride, offset)
this.gl.uniform2f(this.rectangleResolutionLocation, this.gl.canvas.width, this.gl.canvas.height)
// Color
var colorArray = color.getColor()
this.gl.uniform4fv(this.rectangleColorLocation, colorArray)
// Draw rectangle
var primitiveType = this.gl.TRIANGLE_FAN
// var primitiveType = this.gl.POINTS
var offset = 0
var count = circleVertices.length / size
// var count = positions.length / size
this.gl.drawArrays(primitiveType, offset, count)
}
WebGLRenderer.prototype.render = function (time) {
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height)
var delta = Math.sin(time / 1000) * 10
this.clearCanvas(new Color(0, 0, 0, 255))
var rectangleColor = new Color(0, 65, 255, 255)
var width = 50
var height = 50
var circleColor = new Color(0, 167, 255, 255)
this.drawCircle(10, 10, 10, circleColor)
requestAnimationFrame(this.render.bind(this))
}
return WebGLRenderer
})()
function Color(r, g, b, a) {
this.r = r
this.g = g
this.b = b
this.a = a
this.getColor = function () {
return [r / 255, g / 255, b / 255, a / 255]
}
}
var renderer = new WebGLRenderer()
Results: blurry circle (everything I render with WebGL is blurry)
See fiddle for results: https://jsfiddle.net/xLwmngav/1/
Expected results: a smooth round circle
Any help is appreciated. Thank you in advance.
As is pointed out in this article canvases have 2 sizes, their resolution (how many pixels are in them) and the size they are displayed.
Generally you want the resolution to match or exceed the size the canvas is displayed. The best way to do that is to to check, just before rendering, if the canvas's resolution matches the size it's displayed and if it's not to resize it with a function like this
function resize(canvas) {
// Lookup the size the browser is displaying the canvas.
const desiredWidth = canvas.clientWidth;
const desiredHeight = canvas.clientHeight;
// Check if the canvas is not the same size.
if (canvas.width !== desiredWidth ||
canvas.height !== desiredHeight) {
// Make the canvas the same size
canvas.width = desiredWidth;
canvas.height = desiredHeight;
}
}
And use it like this
function render() {
resize(canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
... draw here ...
...
example:
function resize(canvas) {
// Lookup the size the browser is displaying the canvas.
const desiredWidth = canvas.clientWidth;
const desiredHeight = canvas.clientHeight;
// Check if the canvas is not the same size.
if (canvas.width !== desiredWidth ||
canvas.height !== desiredHeight) {
// Make the canvas the same size
canvas.width = desiredWidth;
canvas.height = desiredHeight;
}
}
var WebGLRenderer = (function () {
function WebGLRenderer() {
this.canvas = document.getElementById('canvas')
this.gl = this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl')
if (!this.gl) {
throw Error('Your browser does not support WebGL')
return
}
// Programs
this.rectangleProgram = webglUtils.createProgramFromScripts(this.gl, ['2d-vertex-shader', '2d-fragment-shader'])
// Locations
this.rectanglePoisitionLocation = this.gl.getAttribLocation(this.rectangleProgram, 'a_position')
// Uniforms
this.rectangleResolutionLocation = this.gl.getUniformLocation(this.rectangleProgram, 'u_resolution')
this.rectangleColorLocation = this.gl.getUniformLocation(this.rectangleProgram, 'u_color')
// this.positionBuffer = this.gl.createBuffer()
this.rectanglePositionBuffer = this.gl.createBuffer()
// this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer)
requestAnimationFrame(this.render.bind(this))
}
WebGLRenderer.prototype.clearCanvas = function (color) {
var rgba = color.getColor()
this.gl.clearColor(...rgba)
this.gl.clear(this.gl.COLOR_BUFFER_BIT)
}
WebGLRenderer.prototype.drawCircle = function (x, y, radius, color) {
// Render circle
// For now user rectangleProgram
this.gl.useProgram(this.rectangleProgram)
this.gl.enableVertexAttribArray(this.rectanglePoisitionLocation)
// this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer)
this.circleBuffer = this.gl.createBuffer()
// this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.rectanglePositionBuffer)
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.circleBuffer)
// Setup circle
var circleVertices = [x, y]
var numFans = 360
var anglePerFan = (2 * Math.PI) / numFans
for (var i = 0; i <= numFans; i++) {
var angle = anglePerFan * (i + 1)
var angledX = x + Math.cos(angle) * radius
var angledY = y + Math.sin(angle) * radius
circleVertices.push(angledX, angledY)
// circleVertices.push()
}
/*var circleVertices = [
x, y,
15, 18,
5, 18,
0, 10,
4, 1,
14, 1,
20, 9,
15, 18
]*/
// three 2d points
// TODO: Research static draw
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(circleVertices), this.gl.DYNAMIC_DRAW)
// this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW)
var size = 2
var type = this.gl.FLOAT
var normalize = false
var stride = 0
var offset = 0
this.gl.vertexAttribPointer(this.rectanglePoisitionLocation, size, type, normalize, stride, offset)
this.gl.uniform2f(this.rectangleResolutionLocation, this.gl.canvas.width, this.gl.canvas.height)
// Color
var colorArray = color.getColor()
this.gl.uniform4fv(this.rectangleColorLocation, colorArray)
// Draw rectangle
var primitiveType = this.gl.TRIANGLE_FAN
// var primitiveType = this.gl.POINTS
var offset = 0
var count = circleVertices.length / size
// var count = positions.length / size
this.gl.drawArrays(primitiveType, offset, count)
}
WebGLRenderer.prototype.render = function (time) {
resize(this.gl.canvas);
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height)
var delta = Math.sin(time / 1000) * 10
this.clearCanvas(new Color(0, 0, 0, 255))
var rectangleColor = new Color(0, 65, 255, 255)
var width = 50
var height = 50
var circleColor = new Color(0, 167, 255, 255)
this.drawCircle(10, 10, 10, circleColor)
requestAnimationFrame(this.render.bind(this))
}
return WebGLRenderer
})()
function Color(r, g, b, a) {
this.r = r
this.g = g
this.b = b
this.a = a
this.getColor = function () {
return [r / 255, g / 255, b / 255, a / 255]
}
}
var renderer = new WebGLRenderer()
window.WebGLRenderer = WebGLRenderer
body {
margin: 0;
}
#canvas {
display: block; /* prevents scrollbar */
width: 100vw;
height: 100vh;
}
<canvas id="canvas"></canvas>
<!-- vertex shader -->
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
uniform vec2 u_resolution;
void main() {
// convert the rectangle points from pixels to 0.0 to 1.0
vec2 zeroToOne = a_position / u_resolution;
// convert from 0->1 to 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// convert from 0->2 to -1->+1 (clipspace)
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
}
</script>
<!-- fragment shader -->
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
</script>
<script src="https://webglfundamentals.org/webgl/resources/webgl-utils.js"></script>
<script src="https://webglfundamentals.org/webgl/resources/m3.js"></script>
The canvas defaults to a width and height of 300x150 pixels. These are attributes of the <canvas> element, not CSS properties. By scaling to 100vw by 100vh, you're just stretching those 300x150 pixels out to fill the screen.
To actually get a 1:1 mapping from canvas pixels to screen pixels, you need to set the width and height of the canvas to the size of the window:
this.canvas.width = window.innerWidth
this.canvas.height = window.innerHeight
You may also want to listen for the resize event on window and update the canvas size accordingly.
Fiddle: https://jsfiddle.net/kL1a2zpr/

Apply color gradient to material on mesh - three.js

I have an STL file loaded into my scene with a single colour applied to a phong material
I'd like a way of applying two colours to this mesh's material with a gradient effect applied on the Z axis a like the example below.Gradient Vase]1
I have a feeling I may have to introduce shaders but I've not gotten this far with three.js.
Simple gradient shader, based on uvs:
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, 1, 1, 1000);
camera.position.set(13, 25, 38);
camera.lookAt(scene.position);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
var canvas = renderer.domElement
document.body.appendChild(canvas);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
var geometry = new THREE.CylinderBufferGeometry(2, 5, 20, 32, 1, true);
var material = new THREE.ShaderMaterial({
uniforms: {
color1: {
value: new THREE.Color("red")
},
color2: {
value: new THREE.Color("purple")
}
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}
`,
fragmentShader: `
uniform vec3 color1;
uniform vec3 color2;
varying vec2 vUv;
void main() {
gl_FragColor = vec4(mix(color1, color2, vUv.y), 1.0);
}
`,
wireframe: true
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
render();
function resize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render() {
if (resize(renderer)) {
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
html,
body {
height: 100%;
margin: 0;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
display;
block;
}
<script src="https://cdn.jsdelivr.net/npm/three#0.115.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three#0.115.0/examples/js/controls/OrbitControls.js"></script>
Simple gradient shader, based on coordinates:
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, 1, 1, 1000);
camera.position.set(13, 25, 38);
camera.lookAt(scene.position);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
var canvas = renderer.domElement
document.body.appendChild(canvas);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
var geometry = new THREE.CylinderBufferGeometry(2, 5, 20, 16, 4, true);
geometry.computeBoundingBox();
var material = new THREE.ShaderMaterial({
uniforms: {
color1: {
value: new THREE.Color("red")
},
color2: {
value: new THREE.Color("purple")
},
bboxMin: {
value: geometry.boundingBox.min
},
bboxMax: {
value: geometry.boundingBox.max
}
},
vertexShader: `
uniform vec3 bboxMin;
uniform vec3 bboxMax;
varying vec2 vUv;
void main() {
vUv.y = (position.y - bboxMin.y) / (bboxMax.y - bboxMin.y);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}
`,
fragmentShader: `
uniform vec3 color1;
uniform vec3 color2;
varying vec2 vUv;
void main() {
gl_FragColor = vec4(mix(color1, color2, vUv.y), 1.0);
}
`,
wireframe: true
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
render();
function resize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render() {
if (resize(renderer)) {
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
html,
body {
height: 100%;
margin: 0;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
<script src="https://cdn.jsdelivr.net/npm/three#0.115.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three#0.115.0/examples/js/controls/OrbitControls.js"></script>
Gradient with vertex colours:
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, 1, 1, 1000);
camera.position.set(0, 0, 10);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
var canvas = renderer.domElement
document.body.appendChild(canvas);
var geom = new THREE.TorusKnotGeometry(2.5, .5, 100, 16);
var rev = true;
var cols = [{
stop: 0,
color: new THREE.Color(0xf7b000)
}, {
stop: .25,
color: new THREE.Color(0xdd0080)
}, {
stop: .5,
color: new THREE.Color(0x622b85)
}, {
stop: .75,
color: new THREE.Color(0x007dae)
}, {
stop: 1,
color: new THREE.Color(0x77c8db)
}];
setGradient(geom, cols, 'z', rev);
function setGradient(geometry, colors, axis, reverse) {
geometry.computeBoundingBox();
var bbox = geometry.boundingBox;
var size = new THREE.Vector3().subVectors(bbox.max, bbox.min);
var vertexIndices = ['a', 'b', 'c'];
var face, vertex, normalized = new THREE.Vector3(),
normalizedAxis = 0;
for (var c = 0; c < colors.length - 1; c++) {
var colorDiff = colors[c + 1].stop - colors[c].stop;
for (var i = 0; i < geometry.faces.length; i++) {
face = geometry.faces[i];
for (var v = 0; v < 3; v++) {
vertex = geometry.vertices[face[vertexIndices[v]]];
normalizedAxis = normalized.subVectors(vertex, bbox.min).divide(size)[axis];
if (reverse) {
normalizedAxis = 1 - normalizedAxis;
}
if (normalizedAxis >= colors[c].stop && normalizedAxis <= colors[c + 1].stop) {
var localNormalizedAxis = (normalizedAxis - colors[c].stop) / colorDiff;
face.vertexColors[v] = colors[c].color.clone().lerp(colors[c + 1].color, localNormalizedAxis);
}
}
}
}
}
var mat = new THREE.MeshBasicMaterial({
vertexColors: THREE.VertexColors,
wireframe: true
});
var obj = new THREE.Mesh(geom, mat);
scene.add(obj);
render();
function resize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render() {
if (resize(renderer)) {
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
obj.rotation.y += .01;
requestAnimationFrame(render);
}
html,
body {
height: 100%;
margin: 0;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
display;
block;
}
<script src="https://cdn.jsdelivr.net/npm/three#0.115.0/build/three.min.js"></script>
Actually, it's up to you which approach to use: shaders, vertex colours, textures etc.
If you want to keep the functionality of the MeshPhongMaterial you can try extending the material.
This is a somewhat broad topic with several approaches, and you can read more about it in depth here.
There is a line in the phong materials shader that looks like this
vec4 diffuseColor = vec4( diffuse, opacity );
So after studying the book of shaders or some other tutorials, you will learn that you can mix two colors by using a normalized factor ( a number between 0,1).
That means that you could change this line to something like this
vec4 diffuseColor = vec4( mix(diffuse, myColor, vec3(myFactor)), opacity);
You can extend the shader as such
const myFactor = { value: 0 }
const myColor = {value: new THREE.Color}
myMaterial.onBeforeCompile = shader=>{
shader.uniforms.myFactor = myFactor
shader.uniforms.myColor = myColor
shader.fragmentShader = `
uniform vec3 myColor;
uniform float myFactor;
${shader.fragmentShader.replace(
vec4 diffuseColor = vec4( diffuse, opacity );
vec4 diffuseColor = vec4( mix(diffuse, myColor, vec3(myFactor)), opacity);
)}
`
Now when you change myFactor.value the color of your object should change from myMaterial.color to myColor.value.
Now to actually make it into a gradient you would replace myFactor with something dynamic. I like prisoners solution to use the uvs. It's entirely done in javascript, and very simple to hook up in this shader. Other approaches would probably require more shader work.
vec4 diffuseColor = vec4( mix(diffuse, myColor, vec3(vUv.y)), opacity);
Now the problem you may encounter - if you call new PhongMaterial({color}) ie. without any textures provided to it, the shader will compile without vUv.
There are many conditions that would cause it to compile and be useful to you, but i'm not sure if they break other stuff:
#if defined( USE_MAP ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( USE_SPECULARMAP ) || defined( USE_ALPHAMAP ) || defined( USE_EMISSIVEMAP ) || defined( USE_ROUGHNESSMAP ) || defined( USE_METALNESSMAP )
So, adding something like
myMaterial.defines = {USE_MAP:''}
Might make vUv variable available for your shader. This way you get all the lights of the phong material to affect the material, you just change the base color.
If you want your gradient to be static, you could just add a texture to your material using the .map property. Or you could assign it to the .emissiveMap property if you want it to "glow" without the need of lights.
However, if you want your gradient to change, and always fade in the z-axis, even after rotating the model or camera, you'd have to write a custom shader, which would require you to take some tutorials. You could look at this example for how to implement custom shaders in Three.js, and visit https://thebookofshaders.com/ to get a good understanding on how to write a simple gradient shader.

Three.js: Setting `texture.needsUpdate = true` is very slow

I'm working on a Three.js scene in which I'd like to update some textures after some time. I'm finding that updating the textures is very slow, however, and drags FPS to only 1-2 FPS for several seconds (when updating just a single texture).
Is there anything one can do to expedite texture updates? Any insights others can offer on this question would be very appreciated.
To see this behavior, click the window of the example below. This will load the first texture update (another click will trigger the second texture update). If you try to zoom after one of these clicks, you'll find the screen freezes and the FPS will drop terribly. Does anyone know how to fix this problem?
<html>
<head>
<style>
html, body { width: 100%; height: 100%; background: #000; }
body { margin: 0; overflow: hidden; }
canvas { width: 100%; height: 100%; }
</style>
</head>
<body>
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/88/three.min.js'></script>
<script src='https://rawgit.com/YaleDHLab/pix-plot/master/assets/js/trackball-controls.js'></script>
<script src='https://rawgit.com/mrdoob/stats.js/master/build/stats.min.js'></script>
<script type='x-shader/x-vertex' id='vertex-shader'>
precision highp float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform vec3 cameraPosition;
attribute vec3 position; // sets the blueprint's vertex positions
attribute vec3 translation; // x y translation offsets for an instance
attribute float texIdx; // the texture index to access
varying float vTexIdx;
void main() {
// set point position
vec3 pos = position + translation;
vec4 projected = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
gl_Position = projected;
// assign the varyings
vTexIdx = texIdx;
// use the delta between the point position and camera position to size point
float xDelta = pow(projected[0] - cameraPosition[0], 2.0);
float yDelta = pow(projected[1] - cameraPosition[1], 2.0);
float zDelta = pow(projected[2] - cameraPosition[2], 2.0);
float delta = pow(xDelta + yDelta + zDelta, 0.5);
gl_PointSize = 40000.0 / delta;
}
</script>
<script type='x-shader/x-fragment' id='fragment-shader'>
precision highp float;
uniform sampler2D a;
uniform sampler2D b;
varying float vTexIdx;
void main() {
int textureIndex = int(vTexIdx);
vec2 uv = vec2(gl_PointCoord.x, gl_PointCoord.y);
if (textureIndex == 0) {
gl_FragColor = texture2D(a, uv);
} else if (textureIndex == 1) {
gl_FragColor = texture2D(b, uv);
}
}
</script>
<script>
/**
* Generate a scene object with a background color
**/
function getScene() {
var scene = new THREE.Scene();
scene.background = new THREE.Color(0xaaaaaa);
return scene;
}
/**
* Generate the camera to be used in the scene
**/
function getCamera() {
var aspectRatio = window.innerWidth / window.innerHeight;
var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 100000);
camera.position.set(0, 1, -6000);
return camera;
}
/**
* Generate the renderer to be used in the scene
**/
function getRenderer() {
// Create the canvas with a renderer
var renderer = new THREE.WebGLRenderer({antialias: true});
// Add support for retina displays
renderer.setPixelRatio(window.devicePixelRatio);
// Specify the size of the canvas
renderer.setSize(window.innerWidth, window.innerHeight);
// Add the canvas to the DOM
document.body.appendChild(renderer.domElement);
return renderer;
}
/**
* Generate the controls to be used in the scene
**/
function getControls(camera, renderer) {
var controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.zoomSpeed = 0.4;
controls.panSpeed = 0.4;
return controls;
}
/**
* Generate the points for the scene
**/
function addPoints(scene) {
var BA = THREE.BufferAttribute;
var IBA = THREE.InstancedBufferAttribute;
var geometry = new THREE.InstancedBufferGeometry();
// add data for each observation
var n = 10000; // number of observations
var rootN = n**(1/2);
var cellSize = 20;
var translation = new Float32Array( n * 3 );
var texIdx = new Float32Array( n );
var translationIterator = 0;
var texIterator = 0;
for (var i=0; i<n*3; i++) {
var x = Math.random() * n - (n/2);
var y = Math.random() * n - (n/2);
translation[translationIterator++] = x;
translation[translationIterator++] = y;
translation[translationIterator++] = Math.random() * n - (n/2);
texIdx[texIterator++] = (x + y) > (n/8) ? 1 : 0;
}
var positionAttr = new BA(new Float32Array( [0, 0, 0] ), 3);
var translationAttr = new IBA(translation, 3, 1);
var texIdxAttr = new IBA(texIdx, 1, 1);
positionAttr.dynamic = true;
translationAttr.dynamic = true;
texIdxAttr.dynamic = true;
geometry.addAttribute('position', positionAttr);
geometry.addAttribute('translation', translationAttr);
geometry.addAttribute('texIdx', texIdxAttr);
var canvases = [
getElem('canvas', { width: 16384, height: 16384, }),
getElem('canvas', { width: 16384, height: 16384, }),
]
var textures = [
getTexture( canvases[0] ),
getTexture( canvases[1] ),
];
var material = new THREE.RawShaderMaterial({
uniforms: {
a: {
type: 't',
value: textures[0],
},
b: {
type: 't',
value: textures[1],
}
},
vertexShader: document.getElementById('vertex-shader').textContent,
fragmentShader: document.getElementById('fragment-shader').textContent,
});
var mesh = new THREE.Points(geometry, material);
mesh.frustumCulled = false; // prevent the mesh from being clipped on drag
scene.add(mesh);
// on the first window click, paint red points
// on the second window click, paint blue points
var clicks = 0;
window.addEventListener('click', function() {
if (clicks == 0 || clicks == 1) {
var canvas = canvases[clicks];
var ctx = canvas.getContext('2d');
ctx.fillStyle = clicks == 0 ? 'red' : 'blue';
ctx.rect(0, 0, 16384, 16384);
ctx.fill();
textures[clicks].needsUpdate = true;
clicks++;
}
})
}
function getTexture(canvas) {
var tex = new THREE.Texture(canvas);
tex.needsUpdate = true;
tex.flipY = false;
return tex;
}
/**
* Create an element
**/
function getElem(tag, obj) {
var obj = obj || {};
var elem = document.createElement(tag);
Object.keys(obj).forEach(function(attr) {
elem[attr] = obj[attr];
})
return elem;
}
/**
* Add stats
**/
function getStats() {
var stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '65px';
stats.domElement.style.right = '5px';
stats.domElement.style.left = 'initial';
document.body.appendChild(stats.domElement);
return stats;
}
/**
* Render!
**/
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
controls.update();
stats.update();
};
/**
* Main
**/
var stats = getStats();
var scene = getScene();
var camera = getCamera();
var renderer = getRenderer();
var controls = getControls(camera, renderer);
addPoints(scene);
render();
</script>
</body>
</html>
Your canvases are 16384 by 16384. That's basically insanely large.
For RGBA format, that is 1073741824 bytes.. a gigabyte of texture data that is getting sent to your GPU from the CPU when you set that texture.needsUpdate = true
You will definitely notice this getting uploaded to the card.
If your use case absolutely requires textures that large.. then you may need to consider doing incremental updates via gl.texSubImage2D, or using a bunch of smaller textures and only updating one of the per frame, or only updating those textures at the start of your app, and not thereafter.
For reference, there are very few cases i've seen where textures > 4k per side are needed.
And that is about 1/16th the size of your textures.
This has nothing to do with three.js btw. It's a fundamental characteristic of GPU/CPU interaction. Uploads and state changes are slow and have to be carefully orchestrated and monitored.

Hard Light material blending mode in Three.js?

I am currently using the MeshPhongMaterial provided by Three.js to create a simple scene with basic water. I would like for the water material to have the Hard Light blending mode that can be found in applications such as Photoshop. How can I achieve the Hard Light blending modes below on the right?
The right halves of the images above are set to Hard Light in Photoshop. I am trying to recreate that Hard Light blend mode in Three.js.
One lead I have come across is to completely reimplement the MeshPhongMaterial's fragment and vertex shader, but this will take me some time as I am quite new to this.
What is the way to implement a Hard Light blending mode for a material in Three.js?
/*
* Scene config
**/
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10000);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setClearColor(0xffffff);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
camera.position.set(0, 500, 1000);
camera.lookAt(scene.position);
/*
* Scene lights
**/
var spotlight = new THREE.SpotLight(0x999999, 0.1);
spotlight.castShadow = true;
spotlight.shadowDarkness = 0.75;
spotlight.position.set(0, 500, 0);
scene.add(spotlight);
var pointlight = new THREE.PointLight(0x999999, 0.5);
pointlight.position.set(75, 50, 0);
scene.add(pointlight);
var hemiLight = new THREE.HemisphereLight(0xffce7a, 0x000000, 1.25);
hemiLight.position.y = 75;
hemiLight.position.z = 500;
scene.add(hemiLight);
/*
* Scene objects
*/
/* Water */
var waterGeo = new THREE.PlaneGeometry(1000, 1000, 50, 50);
var waterMat = new THREE.MeshPhongMaterial({
color: 0x00aeff,
emissive: 0x0023b9,
shading: THREE.FlatShading,
shininess: 60,
specular: 30,
transparent: true
});
for (var j = 0; j < waterGeo.vertices.length; j++) {
waterGeo.vertices[j].x = waterGeo.vertices[j].x + ((Math.random() * Math.random()) * 30);
waterGeo.vertices[j].y = waterGeo.vertices[j].y + ((Math.random() * Math.random()) * 20);
}
var waterObj = new THREE.Mesh(waterGeo, waterMat);
waterObj.rotation.x = -Math.PI / 2;
scene.add(waterObj);
/* Floor */
var floorGeo = new THREE.PlaneGeometry(1000, 1000, 50, 50);
var floorMat = new THREE.MeshPhongMaterial({
color: 0xe9b379,
emissive: 0x442c10,
shading: THREE.FlatShading
});
for (var j = 0; j < floorGeo.vertices.length; j++) {
floorGeo.vertices[j].x = floorGeo.vertices[j].x + ((Math.random() * Math.random()) * 30);
floorGeo.vertices[j].y = floorGeo.vertices[j].y + ((Math.random() * Math.random()) * 20);
floorGeo.vertices[j].z = floorGeo.vertices[j].z + ((Math.random() * Math.random()) * 20);
}
var floorObj = new THREE.Mesh(floorGeo, floorMat);
floorObj.rotation.x = -Math.PI / 2;
floorObj.position.y = -75;
scene.add(floorObj);
/*
* Scene render
**/
var count = 0;
function render() {
requestAnimationFrame(render);
var particle, i = 0;
for (var ix = 0; ix < 50; ix++) {
for (var iy = 0; iy < 50; iy++) {
waterObj.geometry.vertices[i++].z = (Math.sin((ix + count) * 2) * 3) +
(Math.cos((iy + count) * 1.5) * 6);
waterObj.geometry.verticesNeedUpdate = true;
}
}
count += 0.05;
renderer.render(scene, camera);
}
render();
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r73/three.min.js"></script>
I don't think you're going to get the effect you want.
How do you generate the first image? I assume you just made fuzzy oval in photoshop and picked "hard light"?
If you want the same thing in three.js you'll need to generate a fuzzy oval and apply it in 2d using a post processing effect in three.js
You could generate such an oval by making a 2nd scene in three.js, adding the lights and shining them on a black plane that has no waves that's at the same position as the water is in the original scene. Render that to a rendertarget. You probably want only the spotlight and maybe point light in that scene. In your current scene remove the spotlight for sure. Render that to another render target.
When you're done combine the scenes using a post processing effect that implements hard light
// pseudo code
vec3 partA = texture2D(sceneTexture, texcoord);
vec3 partB = texture2D(lightTexture, texcoord);
vec3 line1 = 2.0 * partA * partB;
vec3 line2 = 1.0 - (1.0 - partA) * (1.0 - partB);
gl_FragCoord = vec4(mix(line2, line1, step(0.5, partA)), 1);
I ended up doing it in the following way thanks to gman's excellent answer. View the code snippet below to see it in action.
As gman described:
I created a WebGLRenderTarget to which the scene is rendered to.
The WebGLRenderTarget is then passed to the ShaderMaterial's uniforms as a texture, together with the window.innerWidth, window.innerHeight and color.
The respective texture coordinates, in relation to the current fragment, are calculated by dividing gl_FragCoord by the window's width and height.
The fragment can now sample what is on screen from the WebGLRenderTarget texture and combine that with the color of the object to output the correct gl_FragColor.
So far it works great. The only thing I am currently looking into is to create a separate scene containing only the objects that are necessary for blending, perhaps cloned. I assume that would be more performant. Currently I am toggling the visibility of the object to be blended in the render loop, before and after it is sent to the WebGLRenderTarget. For a larger scene with more objects, that probably doesn't make much sense and would complicate things.
var conf = {
'Color A': '#cc6633',
'Color B': '#0099ff'
};
var GUI = new dat.GUI();
var A_COLOR = GUI.addColor(conf, 'Color A');
A_COLOR.onChange(function(val) {
A_OBJ.material.uniforms.color = {
type: "c",
value: new THREE.Color(val)
};
A_OBJ.material.needsUpdate = true;
});
var B_COLOR = GUI.addColor(conf, 'Color B');
B_COLOR.onChange(function(val) {
B_OBJ.material.uniforms.color = {
type: "c",
value: new THREE.Color(val)
};
B_OBJ.material.needsUpdate = true;
});
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
var renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0x888888);
renderer.setSize(window.innerWidth, window.innerHeight);
var target = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, {format: THREE.RGBFormat});
document.body.appendChild(renderer.domElement);
camera.position.set(0, 0, 50);
camera.lookAt(scene.position);
var A_GEO = new THREE.PlaneGeometry(20, 20);
var B_GEO = new THREE.PlaneGeometry(20, 20);
var A_MAT = new THREE.ShaderMaterial({
uniforms: {
color: {
type: "c",
value: new THREE.Color(0xcc6633)
}
},
vertexShader: document.getElementById('vertexShaderA').innerHTML,
fragmentShader: document.getElementById('fragmentShaderA').innerHTML
});
var B_MAT = new THREE.ShaderMaterial({
uniforms: {
color: {
type: "c",
value: new THREE.Color(0x0099ff)
},
window: {
type: "v2",
value: new THREE.Vector2(window.innerWidth, window.innerHeight)
},
target: {
type: "t",
value: target
}
},
vertexShader: document.getElementById('vertexShaderB').innerHTML,
fragmentShader: document.getElementById('fragmentShaderB').innerHTML
});
var A_OBJ = new THREE.Mesh(A_GEO, A_MAT);
var B_OBJ = new THREE.Mesh(B_GEO, B_MAT);
A_OBJ.position.set(-5, -5, 0);
B_OBJ.position.set(5, 5, 0);
scene.add(A_OBJ);
scene.add(B_OBJ);
function render() {
requestAnimationFrame(render);
B_OBJ.visible = false;
renderer.render(scene, camera, target, true);
B_OBJ.visible = true;
renderer.render(scene, camera);
}
render();
body { margin: 0 }
canvas { display: block }
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5.1/dat.gui.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r74/three.min.js"></script>
<script type="x-shader/x-vertex" id="vertexShaderA">
uniform vec3 color;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script type="x-shader/x-fragment" id="fragmentShaderA">
uniform vec3 color;
void main() {
gl_FragColor = vec4(color, 1.0);
}
</script>
<script type="x-shader/x-vertex" id="vertexShaderB">
uniform vec3 color;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script type="x-shader/x-fragment" id="fragmentShaderB">
uniform vec3 color;
uniform vec2 window;
uniform sampler2D target;
void main() {
vec2 targetCoords = gl_FragCoord.xy / window.xy;
vec4 a = texture2D(target, targetCoords);
vec4 b = vec4(color, 1.0);
vec4 multiply = 2.0 * a * b;
vec4 screen = 1.0 - 2.0 * (1.0 - a) * (1.0 - b);
gl_FragColor = vec4(mix(screen, multiply, step(0.5, a)));
}
</script>

Three.js / Webgl X-RAY effect

How to achieve an x-ray-style effect in three.js / webgl? Some sort of this
UPD
I need real-time render with this stuff, not a still image. This can be done with shaders, that change density in non-linear way on overlaps based on distance. I briefly understand theory, but have no practice, that is why I need help with this
This is the as Владимир Корнилов's example except I changed the shader a little.
I'm not sure what he was going for with the dot(vNormal, vNormel). Doing abs(dot(vNormal, vec3(0, 0, 1)) will give you something that is brighter when facing toward or away from the view. Making it 1.0 - abs(dot(vNormal, vec3(0, 0, 1)) will flip that so perpendicular to the view is brighter. Then add the pow and it looks better to me but I guess that's subjective
var human;
var $ = document.querySelector.bind(document);
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
var renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
renderer.setClearColor(0x000000, 1.0);
lookAt = scene.position;
lookAt.y = 15;
camera.lookAt(lookAt);
document.body.appendChild(renderer.domElement);
var customMaterial = new THREE.ShaderMaterial(
{
uniforms: {
p: { type: "f", value: 2 },
glowColor: { type: "c", value: new THREE.Color(0x84ccff) },
},
vertexShader: $('#vertexShader').text,
fragmentShader: $('#fragmentShader').text,
side: THREE.DoubleSide,
blending: THREE.AdditiveBlending,
transparent: true,
depthWrite: false
});
var loader = new THREE.ColladaLoader();
loader.options.convertUpAxis = true;
loader.load('http://greggman.github.io/doodles/assets/woman.dae', function (collada) {
dae = collada.scene;
dae.traverse( function ( child ) {
if (child instanceof THREE.Mesh) {
console.log(child);
child.material = customMaterial;
}
} );
dae.scale.x = 0.2;
dae.scale.y = 0.2;
dae.scale.z = 0.2;
human = dae;
scene.add(human);
});
function resize() {
var canvas = renderer.domElement;
var width = canvas.clientWidth;
var height = canvas.clientHeight;
if (canvas.width !== width || canvas.height !== height) {
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
}
// call the render function
function render(time) {
time *= 0.001;
resize();
camera.position.x = -20 * (Math.cos(time));
camera.position.z = (20 * (Math.sin(time)));
camera.position.y = 20;
camera.lookAt(lookAt);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<script src="//cdnjs.cloudflare.com/ajax/libs/three.js/r123/three.min.js"></script>
<script src="//greggman.github.io/doodles/js/three/js/loaders/ColladaLoader.js"></script>
<script id="vertexShader" type="x-shader/x-vertex">
uniform float p;
varying float intensity;
void main()
{
vec3 vNormal = normalize( normalMatrix * normal );
intensity = pow(1.0 - abs(dot(vNormal, vec3(0, 0, 1))), p);
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
</script>
<!-- fragment shader a.k.a. pixel shader -->
<script id="fragmentShader" type="x-shader/x-vertex">
uniform vec3 glowColor;
varying float intensity;
void main()
{
vec3 glow = glowColor * intensity;
gl_FragColor = vec4( glow, 1.0 );
}
</script>
<style>
html, body {
margin: 0;
overflow: hidden;
height: 100%;
}
canvas {
width: 100%;
height: 100%;
}
</style>
Ok, got acceptable result with this:
<!DOCTYPE html>
<html>
<head>
<title>X-ray</title>
<script type="text/javascript" src="js/three.js/build/three.js"></script>
<script type="text/javascript" src="js/three.js/examples/js/loaders/OBJLoader.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script type="text/javascript" src="js/stats.min.js"></script>
<script type="text/javascript" src="js/three.js/examples/js/renderers/SVGRenderer.js"></script>
<script id="vertexShader" type="x-shader/x-vertex">
uniform vec3 viewVector;
uniform float c;
uniform float p;
varying float intensity;
void main()
{
vec3 vNormal = normalize( normalMatrix * normal );
vec3 vNormel = normalize( normalMatrix * viewVector );
intensity = pow( c - dot(vNormal, vNormel), p );
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
</script>
<!-- fragment shader a.k.a. pixel shader -->
<script id="fragmentShader" type="x-shader/x-vertex">
uniform vec3 glowColor;
varying float intensity;
void main()
{
vec3 glow = glowColor * intensity;
gl_FragColor = vec4( glow, 1.0 );
}
</script>
<style>
body {
/* set margin to 0 and overflow to hidden, to go fullscreen */
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<!-- Div which will hold the Output -->
<div id="WebGL-output">
</div>
<!-- Javascript code that runs our Three.js examples -->
<script type="text/javascript">
// once everything is loaded, we run our Three.js stuff.
$(function () {
var mouseX = 0, mouseY = 0;
var human;
camstep = 0;
var stats = initStats();
// create a scene, that will hold all our elements such as objects, cameras and lights.
var scene = new THREE.Scene();
// create a camera, which defines where we're looking at.
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
// create a render and set the size
var renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
renderer.setClearColor(0x000000, 1.0);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMapEnabled = true;
renderer.shadowMapType = THREE.PCFShadowMap;
materialCameraPosition = camera.position.clone();
materialCameraPosition.z += 10;
// position and point the camera to the center of the scene
camera.position.x = -10;
camera.position.y = 0;
camera.position.z = 15;
lookAt = scene.position;
lookAt.y = 15;
camera.lookAt(lookAt);
// add subtle ambient lighting
var ambientLight = new THREE.AmbientLight(0x0c0c0c);
//scene.add(ambientLight);
// add the output of the renderer to the html element
$("#WebGL-output").append(renderer.domElement);
var customMaterial = new THREE.ShaderMaterial(
{
uniforms: {
"c": { type: "f", value: 1.0 },
"p": { type: "f", value: 3 },
glowColor: { type: "c", value: new THREE.Color(0x84ccff) },
viewVector: { type: "v3", value: materialCameraPosition }
},
vertexShader: document.getElementById('vertexShader').textContent,
fragmentShader: document.getElementById('fragmentShader').textContent,
side: THREE.FrontSide,
blending: THREE.AdditiveBlending,
transparent: true,
//opacity: 0.5,
depthWrite: false
});
var manager = new THREE.LoadingManager();
manager.onProgress = function (item, loaded, total) {
console.log(item, loaded, total);
};
var loader = new THREE.OBJLoader(manager);
loader.load('body_anatomy3.obj', function (object) {
console.log(object);
object.traverse(function (child) {
if (child instanceof THREE.Mesh) {
console.log(child);
child.material = customMaterial;
}
});
object.position.y = 4;
object.scale.x = 0.01;
object.scale.y = 0.01;
object.scale.z = 0.01;
human = object;
scene.add(human);
});
// call the render function
var step = 0;
render();
function render() {
stats.update();
camstep += 0.02;
camera.position.x = -20 * (Math.cos(camstep));
camera.position.z = (20 * (Math.sin(camstep)));
camera.position.y = 20;
camera.lookAt(lookAt);
if (human) {
//human.rotation.y += 0.02;
materialCameraPosition = camera.position.clone();
materialCameraPosition.z += 10;
human.traverse(function (child) {
if (child instanceof THREE.Mesh) {
//console.log(child.material.uniforms.viewVector);
child.material.uniforms.viewVector.value =
new THREE.Vector3().subVectors(camera.position, human.position);
}
});
}
//sphere.material.uniforms.viewVector.value = new THREE.Vector3().subVectors(camera.position, sphere.position);
// render using requestAnimationFrame
requestAnimationFrame(render);
renderer.render(scene, camera);
}
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
// Align top-left
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
$("#Stats-output").append(stats.domElement);
return stats;
}
});
</script>
</body>
</html>
At the moment got close result with glow shader based on this demo http://stemkoski.github.io/Three.js/Shader-Glow.html

Categories