I want to introduce several three.js 3D models within different reveal.js slides. So far I achieved it partially. In each slide I introduce:
<section>
<script src="three_r84/model1.js"></script>
<div id="model1" width="950" height="600" data-prevent-swipe></div>
</section>
And model1.js file is the following:
document.addEventListener('DOMContentLoaded', function(event) {
window.requestAnimationFrame = (function() {
return window.requestAnimationFrame;
})();
function animateScene() {
requestAnimationFrame(animateScene);
renderScene();
}
function startScene() {
var canvas = document.getElementById('model1');
render = new THREE.WebGLRenderer({
antialias: true,
alpha: true});
render.setClearColor(0x191919, 0);
var canvasWidth = canvas.getAttribute('width');
var canvasHeight = canvas.getAttribute('height');
render.setSize(canvasWidth, canvasHeight);
canvas.appendChild(render.domElement);
scene = new THREE.Scene();
var aspect = canvasWidth / canvasHeight;
camera = new THREE.PerspectiveCamera(45, aspect);
camera.position.set(15, 10, 15);
camera.lookAt(scene.position);
scene.add(camera);
controls = new THREE.OrbitControls(camera);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
controls.enableZoom = true;
// concrete columns
// define columns geometry
var columnGeometry = new THREE.BoxGeometry(0.25, 3, 0.4);
// define columns colors
function hsl(h, s, l) {
return (new THREE.Color()).setHSL(h, s, l);
}
// define columns position & pass data (geometry, color, position) to create foundation function
{
makeInstanceColumn(columnGeometry, hsl(0 / 8, 1, .5), 0, -0, 0);
}
// create columns
function makeInstanceColumn(columnGeometry, color, x, z, y) {
[THREE.BackSide, THREE.FrontSide].forEach((side) => {
var columnMaterial = new THREE.MeshPhongMaterial({
color,
opacity: 0.5,
transparent: true,
side,
});
var column = new THREE.Mesh(columnGeometry, columnMaterial);
scene.add(column);
column.position.set(x, z, y);
});
}
// light
function addLight(x, z, y) {
var color = 0xFFFFFF;
var intensity = 1;
var light = new THREE.DirectionalLight(color, intensity);
light.position.set(x, z, y);
scene.add(light);
}
addLight(-1, 2, 4);
addLight( 1, -1, -2);
}
function renderScene() {
controls.update();
render.render(scene, camera);
}
startScene();
animateScene();
renderScene();
});
It works, although I had to insert another slide between to consecutive 3D models. Otherwise they appear in the same slide (as an overlay) even though the "canvas"es are created in different "section"s. And also it does not work more than two times. However, the main problem is that I am unable to interact with the first model. The OrbitControl, seems to be linked with the second last 3D model, which slide I have not yet visualize but rotates, pans and zooms it.
I have read that there is a "slidechanged" event listener. I think that I should insert it somewhere in the code but I do not know how and where. I tried subtituting document.addEventListener('DOMContentLoaded', function(event) ... from the beginning of the script with "document.addEventListener('slidechanged', function(event) ... but it did not work. Silly me, I suppose I should use Reveal.on ... but I do not figure how.
BTW I am using a quite old version of three.js (r84). I do not need the full potential of three.js neither I can use it, because I need to show it in an old iPad (iOS 9.3.5) I know there is an example of combining three.js and reveal.js. It uses and even older version (r59) but I can not simply swap my scripts for its scripts, because it does not recognize some of the commands, e.g. BoxGeometry and other stuff.
I would like to do something similar, but I seek for guidance and advice. Does someone face something similar? Any ideas?
Related
I am trying to create a 3 JS extension for Thingworx, but the renderHtml keeps bugging in combination with a 3 JS canvas in it (See code).
//runtime.ts file
renderHtml(): string {
let htmlString = '<div class="widget-content"><canvas></canvas></div>';
return htmlString;
}
afterRender(): void {
const OrbitControls = require('three-orbit-controls')(CourseView);
const OBJLoader = require('three-obj-loader')(CourseView);
var scene = new CourseView.Scene();
var width = this.getProperty('SceneWidth', 0);
var height = this.getProperty('SceneHeight', 0);
var color = this.getProperty('SceneColor', '#000000');
if(width <= 0) { width = window.innerWidth }
if(height <= 0) { height = window.innerHeight }
if(color == undefined){ color = "#000000" }
var ratio = width / height;
var camera = new CourseView.PerspectiveCamera(75, ratio, 0.1, 1000);
camera.position.z = 30;
var cv = this.jqElement.find("canvas").get(0);
console.log(cv);
this.renderer = new CourseView.WebGLRenderer({canvas: cv});
this.renderer.setSize(width, height);
this.renderer.setClearColor("#0000ff");
var control = new OrbitControls(camera, this.renderer.domElement);
const geometry = new CourseView.SphereGeometry( 15, 32, 16 );
const material = new CourseView.MeshBasicMaterial( { color: 0xff00ff, wireframe: true } );
const sphere = new CourseView.Mesh( geometry, material );
scene.add( sphere );
control.addEventListener('change', () => this.myRender(scene, camera));
this.myRender(scene, camera);
}
myRender(scene, camera) {
this.renderer.render(scene, camera);
}
As shown, the WebGLRenderer gets the canvas inside the div with the class widget-content. I need this div, to realize bindings of Thingworks. When I leave out the div, everything works fine. If the div exists to implement bindings, the sphere is not rendered. Moreover, the renderer seems stuck and also has no blue background, despite the clear-color call.
When I click on it (maybe its not updated) the color changes to blue, but still there is no sphere. Does anyone has realized ThreeJS in Thingworx and can show me how they did it? I think maybe the div widget-content does apply some changes to all childern (also my ThreeJS canvas), but I cant tell which changes... Maybe someone knows?
Full code: https://www.toptal.com/developers/hastebin/olelowawih.js
For those of you might having this problem in the future, check your setter, you might want to render there as well and keep track of NaN values...
Background of Question
I am working on a game that is a mix between Europa Universalis 4 and Age of Empires 3. The game is made in JavaScript and utilizes Three.js (r109) library. As of right now I have made randomly generated low-poly terrain with trees and reflective water. In the beginning I want the game to spawn a Navy, represented by a galleon (in screenshot below). I want to make it so when its called to spawn, it will pick a random location within the bounds of the water. The water mesh is represented by a semi-opaque plane spanning the size of the map- with a THREE.Reflector object underneath it. The terrain is also a plane but has been altered using a SimplexNoise heightmap.
The Question
How do I detect if an x and z position intersects with the water mesh and not the terrain mesh? THREE.Raycaster seems to be useful for what I am trying to do, but I wan't to know if there is a better solution. If using THREE.Raycaster is the best option, how would I go about implementing it for this purpose? Should I make an individual THREE.Raycaster for every object I am doing this with? Keep in mind I'm not placing this object with the mouse, I want to place it with a method that checks the position as stated above.
It's difficult to give specific advice without knowing anything at all about your code, but it sounds like all you need to do is create a collision list for your valid water surfaces and then check that when you want to spawn something.
A very simple jsfiddle is here. It creates a "land" mesh (green) and a "water" mesh (blue), adds the "water" mesh to a variable called collisionList. It then calls a spawn function for coordinates diagonally across both surfaces. The function uses a raycaster to check if the coordinates are over the "water" mesh and spawns a red cube if it is.
Here's the code:
window.onload = function() {
var camera = null, land = null, water = null, renderer = null, lights;
var collisionList;
var d, n, scene = null, animID;
n = document.getElementById('canvas');
function load() {
var height = 600, width = 800;
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(60, width/height, 1, 1000);
camera.position.set(0, 0, -10);
camera.lookAt(new THREE.Vector3(0, 0, 0));
scene.add(camera);
lights = [];
lights[0] = new THREE.PointLight(0xffffff, 1, 0);
lights[1] = new THREE.PointLight(0xffffff, 1, 0);
lights[2] = new THREE.PointLight(0xffffff, 1, 0);
lights[0].position.set(0, 200, 0);
lights[1].position.set(100, 200, 100);
lights[2].position.set(-100, -200, -100);
scene.add(lights[0]);
scene.add(lights[1]);
scene.add(lights[2]);
water = new THREE.Mesh(new THREE.PlaneGeometry(7, 7, 10),
new THREE.MeshStandardMaterial({
color: 0x0000ff,
side: THREE.DoubleSide,
}));
water.position.set(0, 0, 0);
scene.add(water);
land = new THREE.Mesh(new THREE.PlaneGeometry(12, 12, 10),
new THREE.MeshStandardMaterial({
color: 0x00ff00,
side: THREE.DoubleSide,
}));
land.position.set(0, 0, 1);
scene.add(land);
renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
n.appendChild(renderer.domElement);
collisionList = [ water ];
for(var i = -6; i < 6; i++)
spawn(i);
animate();
}
function spawn(x) {
var dir, intersect, mesh, ray, v;
v = new THREE.Vector3(x, x, -1);
dir = new THREE.Vector3(0, 0, 1);
ray = new THREE.Raycaster(v, dir.normalize(), 0, 100);
intersect = ray.intersectObjects(collisionList);
if(intersect.length <= 0)
return;
mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1, 1, 1, 1),
new THREE.MeshStandardMaterial({ color: 0xff0000 }));
mesh.position.set(x, x, 0);
scene.add(mesh);
}
function animate() {
if(!scene) return;
animID = requestAnimationFrame(animate);
render();
update();
}
function render() {
if(!scene || !camera || !renderer) return;
renderer.render(scene, camera);
}
function update() {
if(!scene || !camera) return;
}
load();
As for whether this is a smart way to do it, that really depends on the design of the rest of your game.
If your world is procgen then it may be more efficient/less error prone to generate the spawn points (and any other "functional" parts of the world) first and use that to generate the geography instead of the other way around.
I have a functioning Raycaster for a simple painting app. I use it for a "bucket tool" in which the user can click on an object and change its color. It works for geometry objects such as BoxGeometry and CircleGeometry, but I'm struggling to apply it to the children of an ArrowHelper object. Because ArrowHelper isn't a shape and does not possess a geometry attribute, Raycaster does not detect collision with its position when checking scene.children for intersections. However, the children of ArrowHelper objects are always two things: a line and a cone, both of which have geometry, material, and position attributes.
I HAVE TRIED:
Toggling the recursive boolean of the function .intersectObjects(objects: Array, recursive: Boolean, optionalTarget: Array ) to true, so that it includes the children of the objects in the array.
Circumventing the ArrowHelper parent by iterating through scene.children for ArrowHelper objects and adding their lines and cones into a separate array of objects. From there I attempted to check for intersections with only the list of lines and cones, but no intersections were detected.
Raycaster setup:
const runRaycaster = (mouseEvent) => {
... // sets mouse and canvas bounds here
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
for (let i = 0; i < intersects.length; i++) {
// works for GEOMETRY ONLY
// needs modifications for checking ArrowHelpers
intersects[i].object.material.color.set(currentColor);
}
}
};
Here's my attempt to check the lines and cones individually, without the ArrowHelper parent:
let arrowObjectsList = [];
for (let i = 0; i < scene.children.length; i++) {
if (scene.children[i].type === 'ArrowHelper') {
arrowObjectsList.push(scene.children[i].line);
arrowObjectsList.push(scene.children[i].cone);
} else {
console.log(scene.children[i].type);
}
}
console.log(arrowObjectsList); // returns 2 objects per arrow on the canvas
// intersectsArrows always returns empty
const intersectsArrows = raycaster.intersectObjects(arrowObjectsList, true);
SOME NOTES:
Every ArrowHelper, its line, and its cone have uniquely identifiable names so they can be recolored/repositioned/deleted later.
The Raycaster runs with every onMouseDown and onMouseMove event.
Notably, the line and cone children of ArrowHelpers are BufferGeometry and CylinderBufferGeometry, respectively, rather than variations of Geometry. I'm wondering if this has anything to do with it. According to this example from the Three.JS documentation website, BufferGeometry can be detected by Raycaster in a similar fashion.
Setting recursion = true worked for me. Run the simple code below, and click on the arrow head. You will see the intersection information printed to the console. (three.js r125)
let W = window.innerWidth;
let H = window.innerHeight;
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(28, 1, 1, 1000);
camera.position.set(5, 5, 5);
camera.lookAt(scene.position);
scene.add(camera);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 0, -1);
camera.add(light);
const mesh = new THREE.ArrowHelper(
new THREE.Vector3(0, 0, 1),
new THREE.Vector3(0, 0, 0),
2,
0xff0000,
1,
1
);
scene.add(mesh);
function render() {
renderer.render(scene, camera);
}
function resize() {
W = window.innerWidth;
H = window.innerHeight;
renderer.setSize(W, H);
camera.aspect = W / H;
camera.updateProjectionMatrix();
render();
}
window.addEventListener("resize", resize);
resize();
render();
// RAYCASTER STUFF
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
renderer.domElement.addEventListener('mousedown', function(e) {
mouse.set(
(event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1
);
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
console.log(intersects);
});
html,
body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
background: skyblue;
}
<script src="https://threejs.org/build/three.min.js"></script>
After a closer inspection, it was a matter of the set position, not necessarily the arrow. The position of the arrow varied based on user mouse click to specify the start point. However, it still presented several problems: It was very difficult to select the line because the lineWidth value of LineBasicMaterial cannot have any other value besides 1, despite being editable. This is due to a limitation in the OpenGL Core Profile, as addressed in the docs and in this question. Similarly, the cone would not respond to setLength. This limits the customization of the ArrowHelper tool pretty badly.
Because of this, I decided to entirely replace ArrowHelper with two objects coupled together: tubeGeometry and coneGeometry, both assigned a MeshBasicMaterial, in a way which can be accessed by Raycasters out of the box.
... // the pos Float32Array is set according to user mouse coordinates.
const v1 = new THREE.Vector3(pos[0], pos[1], pos[2]);
const v2 = new THREE.Vector3(pos[3], pos[4], pos[5]);
const material = new THREE.MeshBasicMaterial({
color: color,
side: THREE.DoubleSide,
});
// Because there are only two vectors, no actual curve occurs.
// Therefore, it's our straight line.
const tubeGeometry = new THREE.TubeBufferGeometry(
new THREE.CatmullRomCurve3([v1, v2]), 1, 3, 3, false);
const coneGeometry = new THREE.ConeGeometry(10, 10, 3, 1, false);
arrowLine = new THREE.Mesh(tubeGeometry, material);
arrowTip = new THREE.Mesh(coneGeometry, material);
// needs names to be updated later.
arrowLine.name = 'arrowLineName';
arrowTip.name = 'arrowTipName';
When placing the arrow, the user will click and drag to specify the start and end point of the arrow, so the arrow and its tip have to be updated with onMouseMove. We have to use Math.atan2 to get the angle in degrees between v1 and v2, with v1 as the center. Subtracting 90 orients the rotation to the default position.
... // on the onMouseMove event, pos is updated with new coords.
const setDirection = () => {
const v1 = new THREE.Vector3(pos[0], pos[1], pos[2]);
const v2 = new THREE.Vector3(pos[3], pos[4], pos[5]);
// copying the v2 pos ensures that the arrow tip is always at the end.
arrowTip.position.copy(v2);
// rotating the arrow tip according to the angle between start and end
// points, v1 and v2.
let angleDegrees = 180 - (Math.atan2(pos[1] - pos[4], pos[3] - pos[0]) * 180 / Math.PI - 90);
const angleRadians = angleDegrees * Math.PI / 180;
arrowTip.rotation.set(0, 0, angleRadians);
// NOT VERY EFFICIENT, but it does the job to "update" the curve.
arrowLine.geometry.copy( new THREE.TubeBufferGeometry(new THREE.CatmullRomCurve3([v1, v2]),1,3,3,false));
scene.add(arrowLine);
scene.add(arrowTip);
};
Out of the box, this "arrow" allows me to select and edit it with Raycaster without a problem. No worrying about line positioning, line thickness, or line length.
I'm studying Three.js and I'm tryng to do my first game: and endless game.
I have read this article and the purpose is to do something very similar.
The protagonist (the hero) is a blue ball that rolls towards the "infinity" and must avoid some obstacles that gradually arise in front of him. The user can avoid these obstacles by guiding the ball to the left or right and jumping (the idea is to use the keyboard and in particular the left/right arrow keys and the space bar to jump).
Here is my idea:
I want to follow the idea of the article but not to copy the code (I want to understand it).
This is what I've done so far:
let sceneWidth = window.innerWidth;
let sceneHeight = window.innerHeight;
let canvas;
let camera;
let scene;
let renderer;
let dom;
let sun;
let hero;
let ground;
let clock;
let spotLight;
let ambientLight;
init();
function init() {
createScene();
showHelpers();
update();
}
/**
* Set up scene.
*/
function createScene() {
clock = new THREE.Clock();
clock.start();
scene = new THREE.Scene();
window.scene = scene;
camera = new THREE.PerspectiveCamera(60, sceneWidth / sceneHeight, 0.1, 1000);
camera.position.set(0, 0, 0);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setClearColor(0x333f47, 1);
renderer.shadowMap.enabled = true;
renderer.shadowMapSoft = true;
renderer.setSize(sceneWidth, sceneHeight);
canvas = renderer.domElement;
document.body.appendChild(canvas);
// const orbitControls = new THREE.OrbitControls(camera, canvas);
addGround();
addHero();
addLight();
camera.position.set(0, -1, 0.6);
camera.lookAt(new THREE.Vector3(0, 0, 0));
window.addEventListener("resize", onWindowResize, false);
}
/**
* Show helper.
*/
function showHelpers() {
const axesHelper = new THREE.AxesHelper(5);
// scene.add(axesHelper);
const spotLightHelper = new THREE.SpotLightHelper(spotLight);
scene.add(spotLightHelper);
}
/**
* Add ground to scene.
*/
function addGround() {
const geometry = new THREE.PlaneGeometry(1, 4);
const material = new THREE.MeshLambertMaterial({
color: 0xcccccc,
side: THREE.DoubleSide
});
ground = new THREE.Mesh(geometry, material);
ground.position.set(0, 1, 0);
ground.receiveShadow = true;
scene.add(ground);
}
/**
* Add hero to scene.
*/
function addHero() {
var geometry = new THREE.SphereGeometry(0.03, 32, 32);
var material = new THREE.MeshLambertMaterial({
color: 0x3875d8,
side: THREE.DoubleSide
});
hero = new THREE.Mesh(geometry, material);
hero.receiveShadow = true;
hero.castShadow = true;
scene.add(hero);
hero.position.set(0, -0.62, 0.03);
}
/**
* Add light to scene.
*/
function addLight() {
// spot light
spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(2, 30, 0);
spotLight.angle = degToRad(10);
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.near = 1;
spotLight.shadow.camera.far = 4000;
spotLight.shadow.camera.fov = 45;
scene.add(spotLight);
// ambient light
ambientLight = new THREE.AmbientLight(0x303030, 2);
scene.add(ambientLight);
}
/**
* Call game loop.
*/
function update() {
render();
requestAnimationFrame(update);
}
/**
* Render the scene.
*/
function render() {
renderer.render(scene, camera);
}
/**
* On window resize, render again the scene.
*/
function onWindowResize() {
sceneHeight = window.innerHeight;
sceneWidth = window.innerWidth;
renderer.setSize(sceneWidth, sceneHeight);
camera.aspect = sceneWidth / sceneHeight;
camera.updateProjectionMatrix();
}
/**
* Degree to radiants
*/
function degToRad(degree) {
return degree * (Math.PI / 180);
}
<script src="https://threejs.org/build/three.min.js"></script>
(JSFiddle)
I'm having several problems, the first is the position of objects and the camera.
I would like to be able to position the plane so that the minor side is positioned at the beginning of the screen (the entire plane must therefore be visible, there must not be a hidden part).
I would like the ball to be positioned horizontally in the middle and vertically almost at the beginning of the floor (in short, as shown in the figure) and with the shadow projected onto the plane. Each object must have the shadow projected onto the plane.
I'm using a spotlight and Lambert materials so the shade should be there, but there is not. Why?
I don't even understand how to position objects.
I understood that the point (0, 0, 0) is the center of the screen.
I would like the ground to be at y=0 and all the other objects are positioned above as if they were resting.
My code works but I don't know if there are better ways to handle object placement.
I would also simplify my life by assigning to sphere radius 1 and not 0.03 and then making the scene "smaller" moving the camera away as zoom-out (I think this is the trick).
So, I need help setting the scene correctly.
That is my first application in ThreeJs so any advice is welcome!
EDIT 1
I changed camera.lookAt(new THREE.Vector3(0, 0, 0)); to camera.lookAt(new THREE.Vector3(0, 0, -5)); and I added spotLight.lookAt(new THREE.Vector3(0, 0, -5));.
This is the result:
Not exactly what I want...
You're right in placing your plane and sphere at 0 on the y-axis. The problem you're having is that you're telling the camera to look straight at (0, 0, 0) when you do
camera.lookAt(0, 0, 0);
so you'll get the ball perfectly centered. What you should do is tell the camera to look a little bit ahead of the sphere. You'll have to tweak the value, but something like this should do the trick:
camera.lookAt(0, 0, -5);
Additionally, your spotlight is pointing straight ahead. When you place it at (2, 30, 0), its effects get lost. You need to point it to where you want:
spotLight.lookAt(0, 0, -5);
I am creating a dynamic cube that can be dynamically changed by scaling its mesh. The issue is, I would like to keep it fixed to the floor when modifying its height. This is a snippet of my code:
function init() {
// Floor position
floor = new THREE.Mesh( shadowGeo, shadowMaterial );
floor.position.y = 0;
floor.rotation.x = - Math.PI / 2;
scene.add( floor );
// Defines the cube and its original position
var BoxGeometry = new THREE.BoxGeometry(50, 50, 50);
var boxMaterial = new THREE.MeshLambertMaterial({color: 0x000088});
cube = new THREE.Mesh(BoxGeometry, boxMaterial);
cube.position.set(0,30,0);
scene.add(cube);
// GUI PANEL INTERACTION
// Now the GUI panel for the interaction is defined
gui = new dat.GUI();
parameters = {
height: 1,
reset: function() {resetCube()}
}
// Define the second folder which takes care of the scaling of the cube
var folder1 = gui.addFolder("Dimensions");
var cubeHeight = folder2.add(parameters, "height").min(0).max(200).step(1);
folder1.open();
// Function taking care of the cube changes
cubeHeight.onChange(function(value){cube.scale.y = value;});
gui.open();
}
// Update cube characteristics
function updateCube() {
cube.scale.y = parameters.y;
}
// Reset cube settings
function resetCube() {
parameters.height = 1;
updateCube();
}
// Rest of the code
I have searched around and I saw this similar topic, but still it does not properly explain how to modify dimensions when the object with a reference floor. Do you know how can I solve this issue?
Changed your .onChange() function to have the cube stay on the ground:
// Function taking care of the cube changes
cubeHeightScale.onChange(
function(value)
{
cube.scale.y = value;
cube.position.y = (cubeHeight * value) / 2;
} );
Here is a fiddle to check the changes live: http://jsfiddle.net/Lsjh965o/
three.js r71