how to add custom material to a fragment in forge viewer - autodesk-forge

I have an extension with such methods, and when I call this method to change the material (color it red), the object becomes transparent and the following errors appear in the console
WebGLRenderer.js:5561 WebGL: INVALID_VALUE: uniform3fv: no array
[.WebGL-0000737C06582900] GL_INVALID_OPERATION: Active draw buffers with missing fragment shader outputs.
setColorToItem = (id) => {
const material = this.createMaterial('#ff0000');
const model = this.viewer.model;
const frags = model.getFragmentList();
model.unconsolidate();
this.tree.enumNodeFragments(
id,
(fragId) => {
frags.setMaterial(fragId, material);
this.viewer.impl.getFragmentProxy(model, fragId).updateAnimTransform();
},
true,
);
this.viewer.impl.invalidate(true);
};
createMaterial = (color) => {
const threeColor = new THREE.Color(color);
const material = new THREE.MeshPhongMaterial({
side: THREE.DoubleSide,
flatShading: true,
color: threeColor,
});
const materials = this.viewer.impl.matman();
materials.addMaterial('CustomMaterial' + color.toString(), material, true);
return material;
};
What could be the problem?
I tried using different materials (MeshBasicMaterial, MeshLambertMaterial)
version forge-viewer 7
version threejs 0.71 as indicated here https://forge.autodesk.com/en/docs/viewer/v7/developers_guide/viewer_basics /
also tried the latest

I tried this with a slightly modified version of your code snippet (as shown below) with the viewer version 7.*, and I can set the color to the material successfully. Is there perhaps any other custom JavaScript logic in your app that could interfere with the new materials? From the error logs it looks like the shader is having issues obtaining the 3 float values (red, green, blue) from the THREE.Color object.
function createMaterial(viewer, color) {
const material = new THREE.MeshPhongMaterial({
side: THREE.DoubleSide,
flatShading: true,
color: new THREE.Color(color),
});
const materials = viewer.impl.matman();
materials.addMaterial('CustomMaterialRed', material, true);
return material;
}
function applyMaterial(model, dbid, material) {
const tree = model.getInstanceTree();
const frags = model.getFragmentList();
tree.enumNodeFragments(
dbid,
(fragid) => frags.setMaterial(fragid, material),
true
);
model.unconsolidate();
}
// ...
let mat = createMaterial(viewer, '#ff0000');
applyMaterial(viewer.model, 1234, mat);

Related

Autodesk Forge Viewer Api Cannot load markups inside screenshot

Good day,
I am using the latest Autodesk forge viewer and I am trying to take a screenshot that also renders my markups. Right now my code takes a screenshot without any markups. Below is my viewer code. I am loading markups Core and markups Gui extensions. Notice the "takeSnapshot(viewer)" function inside onDocumentLoadSuccess(viewerDocument). The function is defined right before the initializer function.
function takeSnapshot(target){
$('#snipViewer').click( () => {
target.getScreenShot(1600, 920, (blobURL) => {
let snip = blobURL;
$('#sniplink').attr("href", snip);
$('#sniplink').html('Not Empty');
$('#sniplink').css({"background-image": `url(${blobURL})`});
});
});
}
//Autodesk Viewer Code
instance.data.showViewer = function showViewer(viewerAccessToken, viewerUrn){
localStorage.setItem("viewerAccessTokentoken", viewerAccessToken);
localStorage.setItem("viewerUrn", viewerUrn);
var viewer;
var options = {
env: 'AutodeskProduction',
api: 'derivativeV2',
getAccessToken: function(onTokenReady) {
var token = viewerAccessToken;
var timeInSeconds = 3600;
onTokenReady(token, timeInSeconds);
}
};
Autodesk.Viewing.Initializer(options, function() {
let htmlDiv = document.getElementById('forgeViewer');
viewer = new Autodesk.Viewing.GuiViewer3D(htmlDiv);
let startedCode = viewer.start();
viewer.setTheme("light-theme");
viewer.loadExtension("Autodesk.CustomDocumentBrowser").then(() => {
viewer.loadExtension("Autodesk.Viewing.MarkupsCore");
viewer.loadExtension("Autodesk.Viewing.MarkupsGui");
});
if (startedCode > 0) {
console.error('Failed to create a Viewer: WebGL not supported.');
$("#loadingStatus").html("Failed to create a Viewer: WebGL not supported.");
return;
}
console.log('Initialization complete, loading a model next...');
});
var documentId = `urn:` + viewerUrn;
var derivativeId = `urn:` + instance.derivativeUrn;
Autodesk.Viewing.Document.load(documentId, onDocumentLoadSuccess, onDocumentLoadFailure);
function onDocumentLoadSuccess(viewerDocument) {
var defaultModel = viewerDocument.getRoot().getDefaultGeometry();
viewer.loadDocumentNode(viewerDocument, defaultModel);
takeSnapshot(viewer);
}
function onDocumentLoadFailure() {
console.error('Failed fetching Forge manifest');
$("#loadingStatus").html("Failed fetching Forge manifest.");
}
}
I have already read this article: https://forge.autodesk.com/blog/screenshot-markups
I have tried doing this method but the instructions are very unclear for me. <div style="width:49vw; height:100vh;display:inline-block;"><canvas id="snapshot" style="position:absolute;"></canvas><button onclick="snaphot();" style="position:absolute;">Snapshot!</button></div>
What is the canvas element here for? Am I supposed to renderToCanvas() when I load the markups extension inside the initialize function or in my screenshot function? Is there some way I can implement the renderToCanvas() without changing too much of what I already am using here? I am not an expert with the viewer API so please if you could help me it would be very much appreciated, I am a beginner please don't skip many steps.
Thank you very much!
Here's a bit more simplified logic for generating screenshots with markups in Forge Viewer, with a bit more explanation on why it needs to be done this way below:
function getViewerScreenshot(viewer) {
return new Promise(function (resolve, reject) {
const screenshot = new Image();
screenshot.onload = () => resolve(screenshot);
screenshot.onerror = err => reject(err);
viewer.getScreenShot(viewer.container.clientWidth, viewer.container.clientHeight, function (blobURL) {
screenshot.src = blobURL;
});
});
}
function addMarkupsToScreenshot(viewer, screenshot) {
return new Promise(function (resolve, reject) {
const markupCoreExt = viewer.getExtension('Autodesk.Viewing.MarkupsCore');
const canvas = document.createElement('canvas');
canvas.width = viewer.container.clientWidth;
canvas.height = viewer.container.clientHeight;
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(screenshot, 0, 0, canvas.width, canvas.height);
markupCoreExt.renderToCanvas(context, function () {
resolve(canvas);
});
});
}
const screenshot = await getViewerScreenshot(viewer);
const canvas = await addMarkupsToScreenshot(viewer, screenshot);
const link = document.createElement('a');
link.href = canvas.toDataURL();
link.download = 'screenshot.png';
link.click();
Basically, the markups extension can only render its markups (and not the underlying 2D/3D scene) into an existing <canvas> element. That's why this is a multi-step process:
You render the underlying 2D/3D scene using viewer.getScreenShot, getting a blob URL that contains the screenshot image data
You create a new <canvas> element
You insert the screenshot into the canvas (in this case we create a new Image instance and render it into the canvas using context.drawImage)
You call the extension's renderToCanvas that will render the markups in the canvas on top of the screenshot image

Implementing Three.js SSAOPass in AFrame

I was able to successfully integrate Threejs Effect composer in aframe as a component by exporting everything as THREE.Effectcomposer, THREE.SSAOPass etc. and adding the effect inside a aframe component and i tweaked the AFrame renderer to update the effects in the scene. OutlinePass from threejs worked fine in this code but SSAO is not working and i don't get any errors. Please someone help me figure out the problem. the code for SSAOPass looks like this
AFRAME.registerComponent('ssao', {
init: function () {
this.el.addEventListener('that', evt => this.onEnter());
this.el.addEventListener('mouseleave', evt => this.onLeave());
setTimeout(() => this.el.emit("that"), 2000);
},
onEnter: function () {
const scene = this.el.sceneEl.object3D;
const camera = this.el.sceneEl.camera;
const renderer = this.el.sceneEl.renderer;
const render = renderer.render;
const composer = new THREE.EffectComposer(renderer);
//let renderPass = new THREE.RenderPass(scene, camera);
//let outlinePass = new THREE.OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera);
const ssaoPass = new THREE.SSAOPass( scene, camera, window.innerWidth, window.innerHeight );
//composer.addPass(renderPass);
//composer.addPass(outlinePass);
ssaoPass.kernelRadius = 16;
composer.addPass( ssaoPass );
// let objects = [];
// this.el.object3D.traverse(node => {
// if (!node.isMesh) return;
// objects.push(node);
// });
// outlinePass.selectedObjects = objects;
// outlinePass.renderToScreen = true;
// outlinePass.edgeStrength = this.data.strength;
// outlinePass.edgeGlow = this.data.glow;
// outlinePass.visibleEdgeColor.set(this.data.color);
// HACK the AFRAME render method (a bit ugly)
const clock = new THREE.Clock();
this.originalRenderMethod = render;
let calledByComposer = false;
renderer.render = function () {
if (calledByComposer) {
render.apply(renderer, arguments);
} else {
calledByComposer = true;
composer.render(clock.getDelta());
calledByComposer = false;
}
};
},
onLeave: function () {
this.el.sceneEl.renderer.render = this.originalRenderMethod;
},
remove: function () {
this.onLeave();
}
});
I have also created a glitch project which i am sharing here. Please feel free to join and collaborate in my project
Edit link: https://glitch.com/edit/#!/accessible-torpid-partridge
Site link:https://accessible-torpid-partridge.glitch.me
Thanks in advance
The code is correct, all you need is to tweak the exposed SSAOShader uniforms: SSAOPass.kernelRadius, SSAOPass.minDistance, SSAOPass.maxDistance - like in the Three.js example.
Keep in mind - the scale in the example is huge, so the values will need to be different in a default aframe scene.
It's a good idea to be able to dynamically update a component (via setAttribute() if you properly handle updates), so you can see what's going on in realtime. Something like I did here - SSAO in a-frame (also based on Don McCurdys gist.
I've used some basic HTML elements, most threejs examples use dat.GUI - it is made for demo / debug tweaks.

Forge Viewer, raster images

Is possible to directly load a raster image (PNG, JPG, TIFF) to Forge Viewer?
I see the Autodesk.PDF add-in that can load PDF, I cant find any Autodesk.IMAGE add-in...
Otherwise I need to prior convert Image into PDF and than load it through Autodesk.PDF.
The Autodesk Forge Viewer is based on Three.js - therefore you can use the Three.js API to load an image/texture, there is no need of a Viewer extension for that.
However it depends what you want to do. In case you just want to load an image in the scene, that code is enough.
const texture = THREE.ImageUtils.loadTexture( "thumbnail256.png" );
const material = new THREE.MeshBasicMaterial({ map : texture });
const geometry = new THREE.PlaneGeometry(5, 20, 32);
const planeMesh = new THREE.Mesh(geometry, material);
const planeMesh.position.set(1, 2, 3);
NOP_VIEWER.overlays.addScene('custom-scene');
NOP_VIEWER.overlays.addMesh(planeMesh, 'custom-scene');
But if you want to apply the texture on an existing element in the loaded scene, you need to proceed like this:
const texture = THREE.ImageUtils.loadTexture( "thumbnail256.png" );
const material = new THREE.MeshBasicMaterial({ map : texture, side: THREE.DoubleSide });
NOP_VIEWER.impl.matman().addMaterial('custom-material', material, true);
const model = NOP_VIEWER.model;
model.unconsolidate(); // If the model is consolidated, material changes won't have any effect
const tree = model.getInstanceTree();
const frags = model.getFragmentList();
const dbids = NOP_VIEWER.getSelection();
for (const dbid of dbids) {
tree.enumNodeFragments(dbid, (fragid) => {
frags.setMaterial(fragid, material);
});
}
NOP_VIEWER.impl.invalidate(true, true, false);
Note you may need to work out the texture uv, depending on the geometry.

Is it possible to dimm a model except the objects NOT affected by setThemingColor?

We use the viewer (version 7) to show the issues on 3d model colouring the affected objects using a user defined palette.
The remainig objects of the model are colored with a gray tone.
To achieve this I'm using the setThemingColor technique: I set the theming color grey for the rootid recursive and then I set the correct theme color to the specific issued object.
All the colors used are THREE.Vector4 with the opacity set to 1. In this way the themingColor is non blended with the "natural" color of the object but it "covers" object.
To improve the user experience we'd like to allow the user to dimm the objects not affected by issue instead of set the gray color using setThemingColor.
And now the question: is it possible to change the color (material?) of a group of objects by specifying a fade level up to the ghost of the viewer hide method while preserving the selection functionality?
I have tried the following approach with no success:
const mat = new THREE.MeshBasicMaterial({ color: 0x00ff00, opacity: 0.925, transparent: false });
(this.viewer as any).impl.getMaterials().addMaterial('ghost-material', mat, true);
const model = (this.viewer as any).model;
const tree = model.getInstanceTree();
const frags = model.getFragmentList();
[19240, 20373, 34364, 58813].map(dbid => {
tree.enumNodeFragments(dbid, (fragid) => {
console.log(fragid);
frags.setMaterial(fragid, mat);
(this.viewer as any).impl.invalidate(true);
}, true);
});
[19240, 20373, 34364, 58813] are valid dbids.
The opacity is almost 1 and the material is not transparent hoping to see something.
I tried to invalidate the viewer for each fragment.
After running this script, the 3d model remains the same.
The 'ghost-material' is regularly registered on matman but does not 'replace' the native one.
Where I am doing wrong?
The most important thing to do is to call the method:
model.unconsolidate(); // If the model is consolidated, material changes won't have any effect
before apply the new material to the fragments as explained in the following post
https://forge.autodesk.com/blog/custom-shader-materials-forge-viewer
The right answer is
const mat = new THREE.MeshBasicMaterial({ color: 0x00ff00, opacity: 0.925, transparent: false });
(this.viewer as any).impl.getMaterials().addMaterial('ghost-material', mat, true);
const model = (this.viewer as any).model;
const tree = model.getInstanceTree();
const frags = model.getFragmentList();
// without this it is all useless
model.unconsolidate();
[19240, 20373, 34364, 58813].map(dbid => {
tree.enumNodeFragments(dbid, (fragid) => {
console.log(fragid);
frags.setMaterial(fragid, mat);
(this.viewer as any).impl.invalidate(true);
}, true);
});
Yes, it's a bit of an advanced topic but you can customize materials assigned to individual objects (fragments). The process would be as follows:
Create a custom, semi-transparent material, e.g., a simple THREE.MeshBasicMaterial, and add it to the viewer's material manager
Use the instance tree to enumerate fragments of all objects in your scene
Use the fragment list to retrieve the original material of each fragment, store its reference somewhere, and set its material to your custom one
The code could look roughly like so:
const mat = new THREE.MeshBasicMaterial({ color: 0x00ff00, opacity: 0.25, transparent: true });
viewer.impl.getMaterials().addMaterial('my-material', mat, true);
const tree = viewer.model.getInstanceTree();
const frags = viewer.model.getFragmentList();
tree.enumNodeFragments(tree.getRootId(), (fragid) => { frags.setMaterial(fragid, mat) }, true);
When needed, repeat the process, and reset each fragment to its original material.

How to change the color of sphere objects dynamically (used SceneBuilder in Autodesk forge)

I am working on the example from Custom models in Forge Viewer blog by Petr Broz. I am facing issue in updating the color of sphere objects dynamically. I am getting the value of sphere's color from a json file like this "color": "#FF0000". I have created 3 spheres and I am getting the color of first sphere for the rest also. Why the color is not updating for the other spheres? If the problem is on using same material then I tried giving the sphereMaterial in array also as shown below. Is that wrong or how can i update the color?
var spherecolor='';
var sphereMaterial = [];
const button = document.getElementById('button-geometry');
button.addEventListener('click', async function () {
const sceneBuilder = await viewer.loadExtension('Autodesk.Viewing.SceneBuilder');
const modelBuilder = await sceneBuilder.addNewModel({ conserveMemory: true, modelNameOverride: 'My Custom Model' });
for (var i = 0; i < numOfSphere;i++) {
addGeometry(modelBuilder, jsonGeomConfig.geom[i].dbId, i);
}
});
function addGeometry(modelBuilder, dbId, i) {
const sphereGeometry = new THREE.BufferGeometry().fromGeometry(new THREE.SphereGeometry(0.05, 8, 10));
//Getting spherecolor from json file
spherecolor = jsonGeomConfig.geom[i].color;
sphereMaterial[i] = new THREE.MeshPhongMaterial({ color: spherecolor });
const sphereTransform = new THREE.Matrix4().compose(
new THREE.Vector3(jsonGeomConfig.geom[i].Position.posX, jsonGeomConfig.geom[i].Position.posY, jsonGeomConfig.geom[i].Position.posZ),
new THREE.Quaternion(0, 0, 0, 1),
new THREE.Vector3(2,2,2)
);
modelBuilder.addMaterial('MyCustomMaterial', sphereMaterial[i]);
const sphereGeomId = modelBuilder.addGeometry(sphereGeometry);
const sphereFragId = modelBuilder.addFragment(sphereGeomId, 'MyCustomMaterial', sphereTransform);
modelBuilder.changeFragmentsDbId(sphereFragId, dbId);
}
Be sure to give the materials with different colors different names ... otherwise it'd get overridden - see this live environment:
modelBuilder.addMaterial('MyCustomMaterial'+i, sphereMaterial[i]);
const sphereGeomId = modelBuilder.addGeometry(sphereGeometry);
const sphereFragId = modelBuilder.addFragment(sphereGeomId, 'MyCustomMaterial'+i, sphereTransform);