HOOPS Communicator makes it easy to create any kind of complex 2D markup via the MarkupManager class which uses an SVG layer on top of the 3D model for most markup types. However, sometimes it can be desirable to create markup that is part of the 3D scene. This allows it to be occluded by other 3D objects and also makes the markup geometry selectable as part of the standard HOOPS Communicator selection mechanism.
The SpriteManager class below is a fairly straightforward implementation of billboarded sprites that are created in HOOPS Communicator as textured rectangular meshes with InstanceModifiers set on them to prevent scaling and rotation.
Usage:
var spriteManager = new SpriteManager(hwv);
Instantiate new SpriteManager object. Generally you only want to have one of those objects at a time in your application.
var spriteid = await spriteManager.createSprite("sprites/marker.png",new Communicator.Point3(10,0,0));
Adds a new sprite to the scene at a given 3D point. The first parameter is either a relative URL to the image or the name associated with the image (see below). The image will be pulled from the server at the specified location. It generally should have an alpha component (e.g. png) and can be non-square. There is an image sample attached to the end of this post.
var spriteid = await spriteManager.createSprite("sprites/marker.png",new Communicator.Point3(10,0,0),1.5,50, myUserData);
Adds a new sprite to the scene at a given 3D Point. In this case the third parameter indicates an optional scale factor for the sprite. The fourth parameter specifies an optional distance value. If this value is specified sprites will only be shown if they are within the specified distance from the camera position (to avoid clutter). The last parameter allows for passing user data which is stored with the sprite and can later be queried (e.g. when a sprite is selected).
this.addImage("marker",”sprites/marker.png”,new Communicator.Point2(0,1));
It is also possible to add the images used for the sprites before creating the actual sprite objects. This allows for specifying an alias name that is used for the image instead of the server path. In addition calling this function ahead of createSprite is required if you want to specify a custom origin for the image (defined in normalized coordinates from -1 to 1.). This can be useful for defining the location of an arrow tip for example in relation to the insertion point of the sprite. In the above case the origin of the sprite would be at the bottom center.
spriteManager.get(spriteid)
Get sprite from spriteid.
spriteManager flushAll()
Removes all sprites
spriteManager.hideAll()
Turns off visibility off all sprites
spriteManager.showAll()
Turns on visibility of all sprites
spriteManager.show(spriteid)
turn on sprite visibility for a single sprite
spriteManager.hide(spriteid)
Hide sprite visibility for a single sprite
spriteManager.findFromNodeId(nodeid)
Find a sprite given its nodeid (e.g. from a selection/pick event)
spriteManager.setAlwaysInFront(infront)
Specifies if sprites should be drawn on top of other geometry. Sprites that have a distance value specified will always be drawn on top.
This is the main code for the SpriteManager class. It has no external dependencies except for the relevant HOOPS Web Viewer libraries. Simply save it into a separate file and include it in your project with your other js files. It is important to note that this code is not production-ready and provided “as-is”. It is really meant as a starting point for your own development and as an example for some of the functionality and concepts used in HOOPS Communicator.
class SpriteManager {
constructor(viewer) {
this._viewer = viewer;
this._meshHash = [];
this._spriteNode = this._viewer.model.createNode(this._viewer.model.getAbsoluteRootNode(), "sprites");
this._imageHash = [];
this._sprites = [];
this._alwaysInFront = true;
this._lastCamPosition = new Communicator.Point3(0,0,0);
this._lastCheckDate = new Date();
this._updateVisibilities();
}
_getImage(url) {
return new Promise((resolve, reject) => {
var img = new Image();
img.addEventListener("load", function () {
resolve({img:img,dims:new Communicator.Point2(this.naturalWidth,this.naturalHeight)});
});
img.src = url;
});
}
_getDataUrl(img) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
return canvas.toDataURL('image/png');
}
_convertDataURIToBinary(dataURI) {
var base64Index = dataURI.indexOf(";base64,") + 8;
var raw = window.atob(dataURI.substring(base64Index));
return Uint8Array.from(Array.prototype.map.call(raw, function(c) {
return c.charCodeAt(0);
}));
}
async addImage(type, url_in, offsets) {
var url;
if (url_in == undefined)
url = type;
else
url = url_in;
var res = await this._getImage(url);
var imgBinary = this._convertDataURIToBinary(this._getDataUrl(res.img));
var imageOptions = {
format: Communicator.ImageFormat.Png,
data: imgBinary,
};
var of = "none";
if (offsets)
of = offsets.x +"@" + offsets.y;
var mesh = this._meshHash[of];
if (mesh == undefined)
{
mesh = await this._createSpriteMesh(offsets);
this._meshHash[of] = mesh;
}
var imageid = await this._viewer.model.createImage(imageOptions);
this._imageHash[type] = {id: imageid, mesh:mesh, dims: res.dims};
}
async _createSpriteMesh (offsets) {
var meshData = new Communicator.MeshData();
meshData.setFaceWinding(Communicator.FaceWinding.Clockwise);
var sizex = 1, sizey= 1;
var xoffset=0,yoffset=0;
if (offsets != undefined)
{
xoffset = offsets.x;
yoffset = offsets.y;
}
meshData.addFaces(
[
-sizex + xoffset, -sizey + yoffset, 0,
-sizex + xoffset, sizey + yoffset, 0,
sizex + xoffset, sizey + yoffset, 0,
-sizex + xoffset, -sizey + yoffset, 0,
sizex + xoffset, sizey + yoffset, 0,
sizex + xoffset, -sizey + yoffset, 0
],
null,
null,
[
0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0,
]
);
return await this._viewer.model.createMesh(meshData);
}
async _createSpriteInstance(mesh) {
var lnode = this._viewer.model.createNode(this._spriteNode);
var myMeshInstanceData = new Communicator.MeshInstanceData(mesh);
var mi = await this._viewer.model.createMeshInstance(myMeshInstanceData, lnode);
this._viewer.model.setInstanceModifier(Communicator.InstanceModifier.AlwaysDraw, [lnode], true);
this._viewer.model.setInstanceModifier(Communicator.InstanceModifier.ScreenOriented, [lnode], true);
this._viewer.model.setInstanceModifier(Communicator.InstanceModifier.DoNotLight, [lnode], true);
this._viewer.model.setInstanceModifier(Communicator.InstanceModifier.DoNotCut, [lnode], true);
this._viewer.model.setInstanceModifier(Communicator.InstanceModifier.DoNotXRay, [lnode], true);
this._viewer.model.setInstanceModifier(Communicator.InstanceModifier.SuppressCameraScale, [lnode], true);
this._viewer.model.setDepthRange([lnode], 0, 0.1);
return lnode;
}
async createSprite(type, pos, scale, visibilityRange, payload) {
scale = (typeof scale !== 'undefined') ? scale : 1.0;
visibilityRange = (typeof visibilityRange !== 'undefined') ? visibilityRange : undefined;
payload = (typeof payload !== 'undefined') ? payload : true;
if (this._imageHash[type] == undefined)
await this.addImage(type);
var lnode = await this._createSpriteInstance(this._imageHash[type].mesh);
if (visibilityRange != undefined)
this._viewer.model.setNodesVisibility([lnode], false);
var tmatrix = new Communicator.Matrix();
var smatrix = new Communicator.Matrix();
var scalex = scale * 0.025;
var scaley = scale * 0.025;
var ar = this._imageHash[type].dims.x / this._imageHash[type].dims.y;
if (ar>1)
scaley*=(1/ar);
if (ar<1)
scalex*=ar;
smatrix.setScaleComponent(scalex,scaley,1);
tmatrix.setTranslationComponent(pos.x, pos.y, pos.z);
var mat = Communicator.Matrix.multiply(smatrix, tmatrix);
this._viewer.model.setNodeMatrix(lnode, mat);
await this._viewer.model.setNodesTexture([lnode], { imageId: this._imageHash[type].id});
this._sprites[lnode] = {nodeid: lnode, range:visibilityRange, center:pos, hidden: false, payload:payload };
return lnode;
}
flushAll() {
var nc = this._viewer.model.getNodeChildren(this._spriteNode);
for (var i = 0; i < nc.length; i++)
this._viewer.model.deleteNode(nc[i]);
this._sprites = [];
}
get(spriteid)
{
return this._sprites[nodeid];
}
hideAll()
{
for (var i in this._sprites)
this.hide(i);
}
showAll()
{
for (var i in this._sprites)
this.show(i);
}
hide(spriteid)
{
var sprite = this._sprites[spriteid];
this._viewer.model.setNodesVisibility([parseInt(spriteid)], false);
}
show(spriteid)
{
var sprite = this._sprites[spriteid];
this._viewer.model.setNodesVisibility([parseInt(spriteid)], true);
}
findFromNodeId(nodeid)
{
var res = this._sprites[nodeid];
if (res == undefined)
{
var parent = hwv.model.getNodeParent(nodeid);
res = this._sprites[parent];
}
return res;
}
getAlwaysInFront()
{
return this._alwaysInFront;
}
setAlwaysInFront(infront)
{
this._alwaysInFront = infront;
for (var i in this._sprites)
{
if (infront || this._sprites[i].range != undefined)
this._viewer.model.setDepthRange([parseInt(i)], 0, 0.1);
else
this._viewer.model.unsetDepthRange([parseInt(i)]);
}
}
_updateVisibilities() {
var _this = this;
setInterval(function () {
var currentTime = new Date();
var pos = _this._viewer.view.getCamera().getPosition();
if (!pos.equals(_this._lastCamPosition)) {
_this._lastCamPosition = pos.copy();
_this._lastCheckDate = new Date();
}
else {
if (_this._lastCheckDate!= undefined && currentTime - _this._lastCheckDate > 0.3) {
_this._lastCheckDate = undefined;
for (var i in _this._sprites) {
var nodeid = parseInt(i);
var s = _this._sprites[i];
if (s.range != undefined && !s.hidden) {
var delta = Communicator.Point3.subtract(pos, s.center);
var l = delta.length();
if (l < s.range)
{
if (!_this._viewer.model.getNodeVisibility(nodeid))
_this._viewer.model.setNodesVisibility([nodeid], true);
}
else
{
if (_this._viewer.model.getNodeVisibility(nodeid))
_this._viewer.model.setNodesVisibility([nodeid], false);
}
}
}
}
}
}, 200);
}
}
Sample Sprite Image: