I've been doing a project to have a redux-saga react pattern to store and display the babylon scene logic, what I thoungt was distributing babylon stuff inside a single js file then export to a react fragment.
My qestion is how can we sent the data generate in babylon js by users, outside of the babylon js file (I have tried things like useState but it seemed that only work on react fragment but my babylon js is only handle for game logic.) I think if I can figure out this, I will be able to do futhur step like conncet with redux-saga.
My purpose is first of all bring the params like the position x,y,z outside createScene.js to be utilized by redux-saga, and if the user refresh the page, the scene he created won't dispear.
React newbie here seeking for suggestion, thanks in advance!
React-babylon hook below
import SceneComponent from 'babylonjs-hook'
import styled from 'styled-components'
import 'App.css'
import { onRender, onSceneReady } from '../hooks/babylonjs/createScene'
const ThreeDEditPageMain = styled.div``
const ThreeDEditPage = () => (
<ThreeDEditPageMain>
<SceneComponent antialias onSceneReady={onSceneReady} onRender={onRender} id="my-canvas" />
</ThreeDEditPageMain>
)
export default ThreeDEditPage
createScene.js below
import {
ActionManager,
ArcRotateCamera,
Color3,
ExecuteCodeAction,
HemisphericLight,
Mesh,
MeshBuilder,
StandardMaterial,
Vector3,
VertexBuffer,
} from '#babylonjs/core'
export const onSceneReady = scene => {
// This creates and positions a free camera (non-mesh)
const camera = new ArcRotateCamera('camera1', 0.4, 0.4, 50, new Vector3(0, 5, -10), scene)
// This targets the camera to scene origin
camera.setTarget(Vector3.Zero())
const canvas = scene.getEngine().getRenderingCanvas()
// This attaches the camera to the canvas
camera.attachControl(canvas, true)
camera.wheelPrecision = 50
// This creates a light, aiming 0,1,0 - to the sky (non-mesh)
const light = new HemisphericLight('light', new Vector3(0, 1, 0), scene)
// Default intensity is 1. Let's dim the light a small amount
light.intensity = 0.7
// Our built-in 'ground' shape.
const ground = MeshBuilder.CreateGround(
'ground',
{ width: 100, height: 100, subdivisions: 100 },
scene,
)
ground.updateFacetData()
// console.log(ground.facetNb)
// Our built-in 'box' shape.
const size = 4
const box = MeshBuilder.CreateBox('box', { size }, scene)
// Move the box upward 1/2 its height
// box.position.y = 1
box.position = new Vector3(size / 2, size / 2, size / 2)
box.bakeCurrentTransformIntoVertices()
box.isPickable = false
const positions = ground.getVerticesData(VertexBuffer.PositionKind)
// console.log(positions)
const snappedPosition = new Vector3()
box.position = snappedPosition
scene.onPointerMove = e => {
const pickingInfo = scene.pick(scene.pointerX, scene.pointerY)
if (pickingInfo.hit && pickingInfo.pickedMesh.name === 'ground') {
snappedPosition.x = Math.round(pickingInfo.pickedPoint.x)
snappedPosition.y = Math.round(pickingInfo.pickedPoint.y)
snappedPosition.z = Math.round(pickingInfo.pickedPoint.z)
}
}
// click action for player
ground.actionManager = new ActionManager(scene)
ground.actionManager.registerAction(
new ExecuteCodeAction(ActionManager.OnPickUpTrigger, () => {
// player clicked
console.log(
`gen a new box at x:${snappedPosition.x}, y:${snappedPosition.y}, z:${snappedPosition.z}`,
)
const genBox = Mesh.CreateBox('box', 4, scene)
genBox.position = new Vector3(snappedPosition.x, snappedPosition.y + 2, snappedPosition.z)
const mat = new StandardMaterial('mat', scene)
mat.diffuseColor = new Color3(Math.random(), Math.random(), Math.random()) // color stuff
genBox.material = mat
}),
)
}
export function onRender(sence) {
}
Related
I followed these examples to make the outline for objects when they are selected:
https://threejs.org/examples/?q=out#webgl_postprocessing_outline
https://github.com/scqilin/three-OutlinePass
No error is found, yet outline does not appear when the object is selected. The highlightSelectedObject function is correcly triggered when an object is selected. selectedObjects is not null.
In my case, THREE.js is installed in the project file. Scene, camera and renderer are instantiated elsewhere.
import * as THREE from "../../build/three.module.js";
import {OutlinePass} from "../../examples/jsm/postprocessing/OutlinePass.js";
import {RenderPass} from "../../examples/jsm/postprocessing/RenderPass.js";
import {EffectComposer} from "../../examples/jsm/postprocessing/EffectComposer.js";
Function:
function highlightSelectedObject(selectedObjects) {
if (selectedObjects != null) {
const scene = project.currentScene.scene;
const camera = project.currentScene.camera;
const renderer = project.renderer;
var composer = new EffectComposer(renderer);
var renderPass = new RenderPass(scene, camera);
var outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera, selectedObjects);
outlinePass.renderToScreen = true;
outlinePass.selectedObjects = selectedObjects;
composer.addPass(renderPass);
composer.addPass(outlinePass);
const params = {
edgeStrength: 2,
edgeGlow: 1,
edgeThickness: 1.0,
pulsePeriod: 0,
usePatternTexture: false
};
outlinePass.edgeStrength = params.edgeStrength;
outlinePass.edgeGlow = params.edgeGlow;
outlinePass.visibleEdgeColor.set(0xffffff);
outlinePass.hiddenEdgeColor.set(0xffffff);
composer.render(scene, camera);
}
}
The path to THREE.js should be correct. Is it a problem with render?
I had a similar issue. Upon looking at another example, I found that setting outlinePass.renderToScreen = true allowed it to work. It might not be there depending what version of the the outlinePass.js you are using. I looked at the code on the deployed example and it is there.
I was able to make this example working https://sbcode.net/threejs/engraving/.
I am now looking to engrave my Mesh previously imported from GLB file in the scene.
Below my code:
const loader = new GLTFLoader();
let sword
loader.load("scene/glb/object.glb", function (gltf) {
sword = gltf.scene; // sword 3D object is loaded
sword.scale.set(1, 1, 1);
sword.position.y = 0;
sword.position.x = 0;
sword.position.z = 0;
engravedMesh = sword.children[0]
engravedCSG = CSG.fromMesh(engravedMesh)
scene.add(sword);
engraving()
});
let font
function engraving() {
const loaderFont = new FontLoader()
loaderFont.load('fonts/helvetiker_regular.typeface.json', function (f) {
font = f
regenerateGeometry()
})
}
function regenerateGeometry() {
let newGeometry
newGeometry = new TextGeometry("AAAAAAAAAAAAAAAAAAAAAAAA", {
font: font,
size: 3,
height: 3,
curveSegments: 2,
})
newGeometry.center()
//bender.bend(newGeometry, 'y', Math.PI / 16)
newGeometry.translate(0, 0, 0)
//scene.add(newGeometry)
const textCSG = CSG.fromGeometry(newGeometry)
var engraved = engravedCSG.subtract(textCSG)
engravedMesh.geometry.dispose()
engravedMesh.geometry = CSG.toMesh(
engraved,
new THREE.Matrix4()
).geometry
}
When I tried to execute to execute it, my scree has frozen.
Is there something I did wrong ?
Finally it works with another glb file.
I guess I built a Sphere on Blender with too high definition.
I've imported a GLTF file with two different meshes. My goal is to give each mesh a material with a unique custom fragment shader using onBeforeCompile. Each mesh has the same type of material (MeshNormalMaterial).
When I try to apply one fragment shader to one material and the other fragment shader to the other material, both materials wind up with the same fragment shader. The fragment shader each material has depends on which material I setup first.
Here's a few pictures showing what I'm talking about:
Below is all the relevant code.
Main code: This is the general structure of my code. I've enclosed the important part between "PRIMARY AREA OF INTEREST" comments. For simplicity, I've replaced my shader code with "..." or a comment describing what it does. They do work as shown in the pictures above.
// Three.JS Canvas
const threeDisplay = document.getElementById("threeDisplay");
// Globals
var displayDimensions = getElemDimensions(threeDisplay); // Uniform
var currentTime = 0; // Uniform
var helix = null; // Mesh
var innerHelix = null; // Mesh
var horseshoe = null; // Mesh
// Set the scene and camera up
const scene = new THREE.Scene();
const camera = initCamera();
// Setup a directional light
const light = new THREE.DirectionalLight( 0xffffff, 1.0 );
light.position.set(-0.2, 1, -0.6);
scene.add(light);
// Setup WebGL renderer
const renderer = initRenderer();
threeDisplay.appendChild( renderer.domElement );
// Load the gltf model
new GLTFLoader().load( "./spiral_pillar_hq_horseshoe.glb", function (object) {
const helixFragmentShaderReplacements = [
{
from: ' ... ',
to: ' // rainbow '
}
];
const horseshoeFragmentShaderReplacements = [
{
from: ' ... ',
to: ' // white '
}
];
//////////////////////////////////////
// PRIMARY AREA OF INTEREST - START //
//////////////////////////////////////
// Turn the horseshoe into a shader.
horseshoe = object.scene.children[1];
var horseshoeGeometry = horseshoe.geometry;
var horseshoeMaterial = shaderMeshMaterial(new THREE.MeshNormalMaterial(), horseshoeGeometry, horseshoeFragmentShaderReplacements);
var horseshoeMesh = new THREE.Mesh(horseshoeGeometry, horseshoeMaterial);
horseshoe = horseshoeMesh;
horseshoe.rotation.z = deg2rad(180); // Re-orient the horseshoe to the correct position and rotation.
horseshoe.position.y = 13;
scene.add(horseshoe);
// Turn the inner helix into a colorful, wiggly shader.
helix = object.scene.children[0];
var helixGeometry = helix.geometry;
var helixMaterial = shaderMeshMaterial(new THREE.MeshNormalMaterial(), helixGeometry, helixFragmentShaderReplacements);
var helixMesh = new THREE.Mesh(helixGeometry, helixMaterial);
helix = helixMesh;
scene.add(innerHelix);
animate();
////////////////////////////////////
// PRIMARY AREA OF INTEREST - END //
////////////////////////////////////
}, undefined, function (error) {
console.error(error);
});
Below are functions which are relevant.
shaderMeshMaterial: Constructs a new material based on the supplied materialType that supports editing the default shader. If it's not initProcessing, then the problem may stem from this function.
// Globals used: displayDimensions
function shaderMeshMaterial(materialType, geometry, fragmentShaderReplacements) {
var material = materialType;
material.onBeforeCompile = function ( shader ) {
// Uniforms
shader.uniforms.time = { value: 0 };
shader.uniforms.resolution = { value: new THREE.Vector2(displayDimensions.width, displayDimensions.height) };
shader.uniforms.bboxMin = { value: geometry.boundingBox.min };
shader.uniforms.bboxMax = { value: geometry.boundingBox.max };
fragmentShaderReplacements.forEach((rep) => {
shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
});
console.log(shader);
material.userData.shader = shader;
}
return material;
}
initRenderer: Sets up the renderer. Just showing you guys the renderer setup I have in case that's important.
// Globals used: displayDimensions
function initRenderer() {
var renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true,
precision: "mediump"
});
renderer.setClearColor( 0x000000, 0);
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( displayDimensions.width, displayDimensions.height );
renderer.shadowMap.enabled = true;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.25;
return renderer;
}
animate: Handles the animation frames.
// Globals used: renderer, currentTime, postprocessing
function animate (timestamp = 0) {
requestAnimationFrame(animate);
resizeRendererToDisplaySize(renderer);
currentTime = timestamp/1000; // Current time in seconds.
scene.traverse( function ( child ) {
if ( child.isMesh ) {
const shader = child.material.userData.shader;
if ( shader ) {
shader.uniforms.time.value = currentTime;
}
}
} );
renderer.render( scene, camera );
postprocessing.composer.render( 0.1 );
};
One last thing to note is that when I inspected the console log of shader from the shaderMeshMaterial function, I can see that the fragment shaders are indeed different as they should be for each material. Also not sure why there are 4 console logs when there should only be 2.
Sorry for all the code, but I did condense it to where all irrelevant code was stripped out. I'm fairly new to Three.JS, so any possible explanations as to why this is happening are much appreciated!
EDIT: Removed vertex shader parameter from shaderMeshMaterial function to keep this question focused on just the fragment shaders. Though this problem does apply to both the vertex and fragment shaders, I figure if you fix one then you'll fix the other.
EDIT 2: Added language identifiers to code snippets. Also I removed the postprocessing function and the problem still persists, so I know the problem isn't caused by that. I've updated the code above to reflect this change. As a happy side effect of removing the postprocessing function, the console.log of the shader variable from shaderMeshMaterial new appears twice in the log (as it should).
EDIT 3: (Implementing WestLangley's suggestion) I tweaked the shaderMeshMaterial function by adding the customProgramCacheKey function. I had to condense the four parameters of shaderMeshMaterial into one for the sake of the customProgramCacheKey function. I believe I implemented the function correctly, but I'm still getting the same result as before where both materials display the same fragment shader.
New "PRIMARY AREA OF INTEREST" code:
horseshoe = object.scene.children[1];
var horseshoeGeometry = horseshoe.geometry;
var meshData = {
materialType: new THREE.MeshNormalMaterial(),
geometry: horseshoeGeometry,
fragmentShaderReplacements: horseshoeFragmentShaderReplacements
}
var horseshoeMaterial = shaderMeshMaterial(meshData);
var horseshoeMesh = new THREE.Mesh(horseshoeGeometry, horseshoeMaterial);
horseshoe = horseshoeMesh;
horseshoe.rotation.z = deg2rad(180); // Re-orient the horseshoe to the correct position and rotation.
horseshoe.position.y = 13;
scene.add(horseshoe);
// Turn the inner helix into a colorful, wiggly shader.
helix = object.scene.children[0];
var helixGeometry = helix.geometry;
var meshData2 = {
materialType: new THREE.MeshNormalMaterial(),
geometry: helixGeometry,
fragmentShaderReplacements: helixFragmentShaderReplacements
}
var helixMaterial = shaderMeshMaterial(meshData2);
var helixMesh = new THREE.Mesh(helixGeometry, helixMaterial);
helix = helixMesh;
scene.add(innerHelix);
animate();
New shaderMeshMaterial code:
// Globals used: displayDimensions
function shaderMeshMaterial(meshData) {
var material = meshData.materialType;
material.onBeforeCompile = function ( shader ) {
// Uniforms
shader.uniforms.time = { value: 0 };
shader.uniforms.resolution = { value: new THREE.Vector2(displayDimensions.width, displayDimensions.height) };
shader.uniforms.bboxMin = { value: meshData.geometry.boundingBox.min };
shader.uniforms.bboxMax = { value: meshData.geometry.boundingBox.max };
meshData.fragmentShaderReplacements.forEach((rep) => {
shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
});
material.customProgramCacheKey = function () {
return meshData;
};
console.log(shader);
material.userData.shader = shader;
}
return material;
}
WestLangley suggestion worked for me!
material.onBeforeCompile = ...
// Make sure WebGLRenderer doesnt reuse a single program
material.customProgramCacheKey = function () {
return UNIQUE_PER_MATERIAL_ID;
};
I believe your mistake is returning meshData from customProgramCacheKey.
I think customProgramCacheKey need concrete identifier like a number or string.
It would be nice to understand what exactly happening and why do we need to specify customProgramCacheKey.
EDIT: I discover that default value for customProgramCacheKey calculated as follow in Threejs source.
customProgramCacheKey() {
return this.onBeforeCompile.toString();
}
Perhaps this is explains this default caching behavior because calling toString on function returns that function body literally as string.
For example consider function const myFunc = () => { return 1 }. Calling myFunc.toString() returns "() => { return 1 }"
So if your calling onBeforeCompile in a for loop you function body as string never change.
I'm facing a very weird experience with WebXR API. WebXR API changes the VR camera properties when I re-enter the VR. The camera somehow cuts my objects (shown below) when I re-enter the VR mode the second, third or nth time.
It always works properly (shown below) when I enter VR first time.
I would like to know why the objects are getting cut on the second/third/nth VR attempt and also how to debug WebXR immersive-vr camera properties.
I'm using very basic WebXR API codes as follows:
window.onload=function()
{
init();
animate();
}
function init()
{
canvas = document.getElementById( 'vw_canvas' );
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
canvasCssWidth = canvas.style.width;
canvasCssHeight = canvas.style.height;
group = new THREE.Object3D();//we create this to make it a parent of camera object.
camera = new THREE.PerspectiveCamera( 75, canvas.width / canvas.height, 1, 10000 );
group.rotation.order = 'XZY';//default is XYZ..we change this to XZY. because in case of XYZ when we rotate the object around its Y axis even the X and Z axis changes. so to avoid that we give higher priority to Z axis.
scene = new THREE.Scene();
group.add(camera);
scene.add(group);
//add more 3D objects to scene
renderer = new THREE.WebGLRenderer({antialias:true, powerPreference: "high-performance"});
renderer.setPixelRatio( canvas.devicePixelRatio );
renderer.setSize( canvas.width, canvas.height );
renderer.xr.enabled = true;
renderer.xr.setReferenceSpaceType( 'local' );
canvas.appendChild(renderer.domElement);
var WEBVR =
{
createButton: function ( renderer )
{
function showEnterXR( /*device*/ ) {
var currentSession = null;
function onSessionStarted( session )
{
session.addEventListener( 'end', onSessionEnded );
renderer.xr.setSession( session );
vrButton.style.backgroundImage="url('icons/noVR.svg')";
document.getElementById("vr-button-tooltip").setAttribute("tooltip","Exit VR");
currentSession = session;
isVRpresenting=true;
openVRMenu();
}
function onSessionEnded( event )
{
currentSession.removeEventListener( 'end', onSessionEnded );
renderer.xr.setSession( null );
vrButton.style.backgroundImage="url('icons/yesVR.svg')";
document.getElementById("vr-button-tooltip").setAttribute("tooltip","Enter VR");
currentSession = null;
isVRpresenting=false;
closeVRMenu();
}
vrButton.style.backgroundImage="url('icons/yesVR.svg')";
document.getElementById("vr-button-tooltip").setAttribute("tooltip","Enter VR");
isVRpresenting=false;
vrButton.onclick = function ()
{
if (runOnlyOnce)
{
makeVRMenuItems();
runOnlyOnce=false;
}
if ( currentSession === null )
{
var sessionInit = { optionalFeatures: [ 'local-floor', 'bounded-floor' ] };
navigator.xr.requestSession( 'immersive-vr', sessionInit ).then( onSessionStarted );
}
else
{
currentSession.end();
}
};
}
function showVRNotFound()
{
vrButton.onclick = function ()
{
//open VR popup that shows devices to use in order to exp VR
};
isVRavailable=false;
vrButton.style.backgroundImage="url('icons/noVR.svg')";
document.getElementById("vr-button-tooltip").setAttribute("tooltip","VR is not supported on your device");
vrButton.onclick = null;
// renderer.xr.setDevice( null );
isVRpresenting=false;
}
if ( 'xr' in navigator )
{
isVRavailable=true;
navigator.xr.isSessionSupported( 'immersive-vr' ).then( function ( supported ) {
supported ? showEnterXR() : showVRNotFound();
} );
}
else
{
vrButton.onclick = function ()
{
//open VR popup that shows devices to use in order to exp VR
};
isVRavailable=false;
vrButton.style.backgroundImage="url('icons/noVR.svg')";
document.getElementById("vr-button-tooltip").setAttribute("tooltip","VR is not supported on you device");
isVRpresenting=false;
}
},
};
WEBVR.createButton( renderer );
}
function animate()
{
renderer.setAnimationLoop( animate );
update();
}
function update()
{
renderer.render( scene, camera );
}
Seems like the WebXR session takes the scene camera parameters for the first time it enters VR. Then, on second and subsequent visits the WebXR session sets the camera to default settings. Hence, in order to update the camera properties to be same as scene camera properties, we need to use this session.updateRenderState({ depthFar: 10000 });.In my case my scene camera had depthFar=10000 but WebXR camera resets the depthFar property to 1000 in the second and subsequent visits in VR, which was the reason of frustum culling (image in question).
I can’t seem to be able to clone an FBX model (FBX downloaded from Mixamo) while retaining animation keyframes.
Have attempted a number of approaches including using the cloneFbx gist (included in the example below); all to no avail. Even placing the entire FBXLoader() function inside a loop does not work as expected since only one of the models will animate at a time.
This issue has been partially addressed here, but I cannot seem to ‘copy’ the animation sequence as answer suggests.
Can anyone point out where I’m going wrong?
Here's a rough example of one of my tests:
Load fbx model and store animation:
var loader = new THREE.FBXLoader();
loader.load( 'models/Walking.fbx', function ( fbx ) {
clip = fbx.animations[ 0 ];
// createVehicle(fbx); // Works! Creates one animated model via FBX
// cloneFbx via: https://gist.github.com/kevincharm/bf12a2c673b43a3988f0f171a05794c1
for (var i = 0; i < 2; i++) {
const model = cloneFbx(fbx);
createVehicle(model);
}
});
Add mixers and actions based on stored clip, add model to scene:
function createVehicle(model){
model.mixer = new THREE.AnimationMixer( model );
mixers.push( model.mixer );
var action = model.mixer.clipAction( clip );
action.play();
model.traverse( function ( child ) {
if ( child.isMesh ) {
child.castShadow = true;
child.receiveShadow = true;
}
});
const x = Math.random() * groundSize - groundSize/2;
const z = Math.random() * groundSize - groundSize/2;
model.position.set(x, 0, z);
const vehicle = new Vehicle(model, x, z);
vehicles.push(vehicle);
scene.add( model );
}
Animation cycle:
if ( mixers.length > 0 ) {
for ( var i = 0; i < mixers.length; i ++ ) {
mixers[ 0 ].update( clock.getDelta() );
}
}
Couldn’t figure out an elegant solution to this. Best I could come up with is creating a loop with the loading sequence inside of it; this is very slow (since the FBX has to be parsed each time).
The key here was having an animation mixer controlling the animated objects as a group as opposed to creating a mixer per animated object.
If anyone can figure out a better solution, I would be super keen to hear it (perhaps using the cloneFbx script properly).
Create mixer, load FBX:
// Create mixer to run animations
mixer = new THREE.AnimationMixer( scene );
// Load fbx
var loader = new THREE.FBXLoader();
for (var i = 0; i < 5; i++) {
loader.load( 'models/Walking.fbx', function ( fbx ) {
mixer.clipAction( fbx.animations[ 0 ], fbx )
.startAt( - Math.random() )
.play();
createVehicle(fbx);
});
}
Create class instances, add to scene:
function createVehicle(model){
const x = Math.random() * groundSize - groundSize/2;
const z = Math.random() * groundSize - groundSize/2;
model.position.set(x, 0, z);
const vehicle = new Vehicle(model, x, z);
vehicles.push(vehicle);
scene.add( model );
}
Draw cycle:
mixer.update( clock.getDelta() );
I found out that SkeletonUtils.clone() works good for me.
https://threejs.org/docs/index.html#examples/en/utils/SkeletonUtils.clone