Multiple instances of the same Image Target

With 8thWall is there the ability to track multiple instances of the same Image Target?

So I could for example detect twice the same image showing in front of the camera simultaneously and then anchor different 3D content on each of them.

It doesn’t seem possible but maybe there is a solution. Thank you.

At this time this isn’t possible. However, I have noted this query with the internal team.

Thank you for your reply :+1:

I honestly think it’s possible, but you need to create a component with a dynamic image target, by using pooling.

Having that feature out of the box? No.

However, I have about 60 image targets in my project but only three image target entities.

Greetings Ian,
I would like to reopen this thread to ask if there has been any update on this? I’m working on a project that could benefit from this functionality.

A work-around that I was hoping to use, is to “change” the image target to view each instance by overlapping a new figure over the top of the original target, in a space that would be designated for it. I’ve yet to get it to function - it simply reveals all instances at once. It seems the image tracking is very generous - meaning, it will track even if changes are made to the target provided the majority of it is intact?

Any ideas? Perhaps an additional image (track two targets simultaneously) could do this? For example, a scene only shows when both targets are recognized, but nothing if there is only one?

Thanks for any insight!

edit - I realize what I’m trying to do is slightly different from the original post. For clarification, I’m hoping to use the same image target to load multiple models, at different times - not simultaneously. The idea is to make a subtle adjustment to the image, physically, to trigger a new model, or, some other trigger to do this - perhaps a secondary image target.

Greetings Rickard,
Interested to hear more about how you’ve gone about doing this, if you’re open to sharing a few details!

The project ended up being pretty big, but dynamic. I will copy-paste parts of the code, not to deliver a working solution, but to point in a general direction of how it can be done.

<!-- index.html ->

<a-scene
    loading-localization
    xrextras-loading
    pool__imagetarget="mixin: imagetarget; size: 3; dynamic: false"
    camera-feed-delegator
...
    renderer="colorManagement:true; webgl2: true"
    xrweb="disableWorldTracking: true"
  >
    <a-assets>
      <a-mixin id="imagetarget" ec-named-image-target></a-mixin>
    </a-assets>
  </a-scene>
// camera-feed-delegator.js

const cameraFeedDelegator = {  
  init() {
    const dynamicTargeting = new DynamicTargeting();

    const addElementChildToImageTargetContainer = ({detail}) => {
          dynamicTargeting.foundTarget(detail.name, detail, this.el.sceneEl, point);
    }

    window.addEventListener('xrimagefound', addElementChildToImageTargetContainer);
  
    window.addEventListener('xrimagelost', ({detail}) => {
      dynamicTargeting.lostTarget(detail.name, this.el.sceneEl);
    });
}

export {cameraFeedDelegator}
// dynamic.targeting.js

export default class DynamicTargeting {
  TWO_SECONDS = 2000;
    targets = [];       // all image targets in the room
  newTargets = [];    // a set of max 10 image targets that 8th Wall is scanning for
  found = new Set();  // all image targets that can be found. The max pool is set to 3 in index.html.
  lost = new Set();   // temporary sets a lost image target to be removed within TWO_SECONDS
  ELEMENT_NAME = 'named-image-target';

  /**
   * Adds image target unless recently lost or already has the target
   */
  foundTarget(imageTargetName, {scaledWidth, scaledHeight}, sceneEl, point) {
    // console.log(this.is, "found target", imageTargetName, point);

    if (this.lost.has(imageTargetName))  {
      this.lost.delete(imageTargetName)
    } else if (!this.found.has(imageTargetName)) {
      this.found.add(imageTargetName);
      this.targets.splice(this.targets.indexOf(imageTargetName), 1);

      let childEl = this._setupImageTargetAction(imageTargetName, point);

      // POOLING
      if (childEl) {
        let imageTargetEl = sceneEl.components.pool__imagetarget.requestEntity();

        if (imageTargetEl) {
          // For scaling
          childEl.components.minDimension = Math.min(scaledWidth, scaledHeight);
          childEl.components.maxDimension = Math.max(scaledWidth, scaledHeight);

          imageTargetEl.setAttribute(this.ELEMENT_NAME, `name: ${imageTargetName}`);  

          imageTargetEl.appendChild(childEl);
        } else {
          this.found.remove(imageTargetName);
          sceneEl.components.pool__imagetarget.returnEntity(imageTargetEl);
        }
      }
    }
  }

  /**
   * Removes image target unless the component is playing (isActive)
   */
  lostTarget(imageTargetName, sceneEl) {
    // console.log(this.is, "lost target", imageTargetName);

    for (const imageTargetComponent of sceneEl.components.pool__imagetarget.usedEls) {
      if (imageTargetComponent.getAttribute(this.ELEMENT_NAME)?.name == imageTargetName
        && !imageTargetComponent.components.isActive) {
        this.lost.add(imageTargetName);

        setTimeout(() => { // Avoids temporary loosing target
          if (this.lost.has(imageTargetName)) { // can be removed in foundTarget()
            this.removeTarget(imageTargetName);
          }
        }, this.TWO_SECONDS);
      }
    }
  }
  
  removeTarget(name) {
    // console.log(this.is, "remove target", name);

    this.lost.delete(name);
    this.found.delete(name);
    this.targets.push(name);

    // Tell 'ec-named-image-target' component that the entity should be removed
    window.dispatchEvent(new CustomEvent(
      'remove-image-target',
      {detail: {name: name}}
    ));
  }
}
// ec-named-image-target.js - set as a component on the three image targets

/*
* A container for image targets. Child entities are appended in dynamic-targeting.js
*/

const ecNamedImageTarget = {
  schema: {
    name: {type: 'string'},
    type: {type: 'string'},     // For debugging
    isActive: {type: 'bool'},   // Changed by child entity and used in 'dynamic-targeting'
  },
  get is() {
    return 'named-image-target'
  },
  init() {
    const {object3D} = this.el;
    
    const onready = () => {
      this.el.sceneEl.removeEventListener('realityready', onready);
      object3D.visible = false;
    }

    this.el.sceneEl.addEventListener('realityready', onready);

    const updatePosition = ({detail}) => {
      if (this.data.name === detail.name) {
        object3D.position.copy(detail.position);
        object3D.quaternion.copy(detail.rotation);
        object3D.scale.set(detail.scale, detail.scale, detail.scale);
        object3D.visible = true;

        this.el.firstElementChild?.classList.add('cantap');
      }
    }
    
    const hideComponent = ({detail}) => {
      if (this.data.name === detail.name) {
        object3D.visible = false;

        this.el.firstElementChild?.classList.remove('cantap');
      }
    }

    const removeComponent = ({detail}) => {
      if (this.data.name === detail.name) {
        object3D.visible = false;
        
        this.data.isActive = false;

        if (this.el.firstElementChild) {
          this.el.firstElementChild.object3D.clear()                  // removes components, like 'material' and 'geometry'
          this.el.firstElementChild.removeFromParent();               // sending an event that detaches the child from the DOM
          this.el.removeChild(this.el.firstElementChild);             // actually removing the child from the DOM
        }
        
        this.data.name = null;

        this.el.sceneEl.components.pool__imagetarget.returnEntity(this.el);
      }
    }

    this.el.sceneEl.addEventListener('xrimageupdated', updatePosition);
    this.el.sceneEl.addEventListener('xrimagelost', hideComponent);
    window.addEventListener('remove-image-target', removeComponent);
  },
}

export {ecNamedImageTarget}