Kitware / vtk-js

Visualization Toolkit for the Web
https://kitware.github.io/vtk-js/
BSD 3-Clause "New" or "Revised" License
1.23k stars 371 forks source link

Use vtkjs to develop MPR rendering successfully and black lines appear #2027

Open itsRaul opened 3 years ago

itsRaul commented 3 years ago

High-level description

1628044094619

Environment

itsRaul commented 3 years ago

vtk.js: 11.1.3

itsRaul commented 3 years ago

image

finetjul commented 3 years ago

I believe that MPR widget does not belong to VTK.js, are you sure the problem comes from VTK.js ? Can you try to reproduce the issue in a codepen/codesandbox/... example to share ? Do you have the issue with different dataset ?

itsRaul commented 3 years ago

I believe that MPR widget does not belong to VTK.js, are you sure the problem comes from VTK.js ? Can you try to reproduce the issue in a codepen/codesandbox/... example to share ? Do you have the issue with different dataset ?

image

The black line problem appeared when loading it for the first time. I developed it with reference to the vue-vtkjs-viewport project. The entire 2D MPR is developed using vtkjs。

jourdain commented 3 years ago

Could this come from some svg overlay that library is defining rather than the raster image that vtk.js is producing?

itsRaul commented 3 years ago

Could this come from some svg overlay that library is defining rather than the raster image that vtk.js is producing?

Could this come from some svg overlay that library is defining rather than the raster image that vtk.js is producing?

The GenericRenderWindow setting background can change the color of the black line. ThinkPad notebooks do not display the black line. The black line and GenericRenderWindow are related, and as the browser window changes, the line changes. I feel very strange.

code: this.genericRenderWindow = vtkGenericRenderWindow.newInstance({ background: [255, 0, 0] });

image

itsRaul commented 3 years ago

Could this come from some svg overlay that library is defining rather than the raster image that vtk.js is producing?

Could this come from some svg overlay that library is defining rather than the raster image that vtk.js is producing?

The GenericRenderWindow setting background can change the color of the black line. ThinkPad notebooks do not display the black line. The black line and GenericRenderWindow are related, and as the browser window changes, the line changes. I feel very strange.

code: this.genericRenderWindow = vtkGenericRenderWindow.newInstance({ background: [255, 0, 0] });

image

this.cachedSlicePlane = [...this.slicePlaneNormal];
this.cachedSliceViewUp = [...this.sliceViewUp];
this.genericRenderWindow = vtkGenericRenderWindow.newInstance({
  background: [255, 0, 0]
});
this.genericRenderWindow.setContainer(this.$refs.container);
let widgets = [];
this.renderWindow = this.genericRenderWindow.getRenderWindow();
this.renderer = this.genericRenderWindow.getRenderer();
if (this.parallel) {
  this.renderer.getActiveCamera().setParallelProjection(true);
}
const oglrw = this.genericRenderWindow.getOpenGLRenderWindow();
oglrw.buildPass(true);
const istyle = vtkInteractorStyleMPRSlice.newInstance();
istyle.setOnScroll(this.onStackScroll)
const inter = this.renderWindow.getInteractor();
inter.setInteractorStyle(istyle);
const istyleVolumeMapper = this.volumes[0].getMapper();
istyle.setVolumeMapper(istyleVolumeMapper);
const range = istyle.getSliceRange();
istyle.setSlice((range[0] + range[1]) / 2);
this.updateVolumesForRendering(this.volumes);
this.updateSlicePlane();
this.onResize();
this.renderer.getActiveCamera().zoom(1.4)
if (this.onCreated) {
  this.onCreated({
    genericRenderWindow: this.genericRenderWindow,
    widgetManager: this.widgetManager,
    container: this.$refs.container,
    widgets,
    volumes: [...this.volumes],
    _component: this
  });
}
vtkInteractorStyleMPRSlice.js
/**
 * Based on the vtk.js's MPR Slice interactor Style, but with improvements.
 */

// Temporarily using a modified version of this interactor to deal with a camera subscription issue
import macro from "vtk.js/Sources/macro";
import vtkMath from "vtk.js/Sources/Common/Core/Math";
import vtkMatrixBuilder from "vtk.js/Sources/Common/Core/MatrixBuilder";
import vtkInteractorStyleManipulator from "vtk.js/Sources/Interaction/Style/InteractorStyleManipulator";
import vtkMouseCameraTrackballRotateManipulator from "vtk.js/Sources/Interaction/Manipulators/MouseCameraTrackballRotateManipulator";
import vtkMouseCameraTrackballPanManipulator from "vtk.js/Sources/Interaction/Manipulators/MouseCameraTrackballPanManipulator";
import vtkMouseCameraTrackballZoomManipulator from "vtk.js/Sources/Interaction/Manipulators/MouseCameraTrackballZoomManipulator";
import vtkMouseRangeManipulator from "vtk.js/Sources/Interaction/Manipulators/MouseRangeManipulator";

// ----------------------------------------------------------------------------
// Global methods
// ----------------------------------------------------------------------------

function boundsToCorners(bounds) {
  return [
    [bounds[0], bounds[2], bounds[4]],
    [bounds[0], bounds[2], bounds[5]],
    [bounds[0], bounds[3], bounds[4]],
    [bounds[0], bounds[3], bounds[5]],
    [bounds[1], bounds[2], bounds[4]],
    [bounds[1], bounds[2], bounds[5]],
    [bounds[1], bounds[3], bounds[4]],
    [bounds[1], bounds[3], bounds[5]]
  ];
}

// ----------------------------------------------------------------------------

function clamp(value, min, max) {
  if (value < min) {
    return min;
  }
  if (value > max) {
    return max;
  }
  return value;
}

// ----------------------------------------------------------------------------
// vtkInteractorStyleMPRSlice methods
// ----------------------------------------------------------------------------

function vtkInteractorStyleMPRSlice(publicAPI, model) {
  // Set our className
  model.classHierarchy.push("vtkInteractorStyleMPRSlice");

  model.trackballManipulator = vtkMouseCameraTrackballRotateManipulator.newInstance(
    {
      button: 1
    }
  );
  model.panManipulator = vtkMouseCameraTrackballPanManipulator.newInstance({
    button: 1,
    shift: true
  });
  model.zoomManipulator = vtkMouseCameraTrackballZoomManipulator.newInstance({
    button: 3
  });

  model.scrollManipulator = vtkMouseRangeManipulator.newInstance({
    scrollEnabled: true,
    dragEnabled: false
  });

  // cache for sliceRange
  const cache = {
    sliceNormal: [0, 0, 0],
    sliceRange: [0, 0],
    slicePosition: [0, 0, 0]
  };

  function updateScrollManipulator() {
    const range = publicAPI.getSliceRange();
    console.log("updating the manipulator", range)
    model.scrollManipulator.removeScrollListener();
    // The Scroll listener has min, max, step, and getValue setValue as params.
    // Internally, it checks that the result of the GET has changed, and only calls SET if it is new.
    model.scrollManipulator.setScrollListener(
      range[0],
      range[1],
      1,
      publicAPI.getSlice,
      publicAPI.setSlice
    );
  }

  function setManipulators() {
    publicAPI.removeAllMouseManipulators();
    publicAPI.addMouseManipulator(model.trackballManipulator);
    publicAPI.addMouseManipulator(model.panManipulator);
    publicAPI.addMouseManipulator(model.zoomManipulator);
    publicAPI.addMouseManipulator(model.scrollManipulator);
    updateScrollManipulator();
  }

  let cameraSub = null;
  let interactorSub = null;
  const superSetInteractor = publicAPI.setInteractor;
  publicAPI.setInteractor = interactor => {
    superSetInteractor(interactor);
    if (cameraSub) {
      cameraSub.unsubscribe();
      cameraSub = null;
    }

    if (interactorSub) {
      interactorSub.unsubscribe();
      interactorSub = null;
    }

    if (interactor) {
      const renderer = interactor.getCurrentRenderer();
      const camera = renderer.getActiveCamera();

      cameraSub = camera.onModified(() => {
        updateScrollManipulator();
        publicAPI.modified();
      });

      interactorSub = interactor.onAnimation(() => {
        const { slabThickness } = model;

        const dist = camera.getDistance();
        const near = dist - slabThickness / 2;
        const far = dist + slabThickness / 2;

        camera.setClippingRange(near, far);
      });
    }
  };

  publicAPI.handleMouseMove = macro.chain(publicAPI.handleMouseMove, () => {
    const renderer = model.interactor.getCurrentRenderer();
    const { slabThickness } = model;
    const camera = renderer.getActiveCamera();
    const dist = camera.getDistance();
    const near = dist - slabThickness / 2;
    const far = dist + slabThickness / 2;

    camera.setClippingRange(near, far);
  });

  const superSetVolumeMapper = publicAPI.setVolumeMapper;
  publicAPI.setVolumeMapper = mapper => {
    if (superSetVolumeMapper(mapper)) {
      const renderer = model.interactor.getCurrentRenderer();
      const camera = renderer.getActiveCamera();
      if (mapper) {
        // prevent zoom manipulator from messing with our focal point
        // TODO: remove the zoom maninipulator instead?
        camera.setFreezeFocalPoint(true);

        // NOTE: Disabling this because it makes it more difficult to switch
        // interactor styles. Need to find a better way to do this!
        //publicAPI.setSliceNormal(...publicAPI.getSliceNormal());
      } else {
        camera.setFreezeFocalPoint(false);
      }
    }
  };

  publicAPI.getSlice = () => {
    const renderer = model.interactor.getCurrentRenderer();
    const camera = renderer.getActiveCamera();
    const sliceNormal = publicAPI.getSliceNormal();

    // Get rotation matrix from normal to +X (since bounds is aligned to XYZ)
    const transform = vtkMatrixBuilder
      .buildFromDegree()
      .identity()
      .rotateFromDirections(sliceNormal, [1, 0, 0]);

    const fp = camera.getFocalPoint();
    transform.apply(fp);
    return fp[0];
  };

  publicAPI.setSlice = slice => {
    const renderer = model.interactor.getCurrentRenderer();
    const camera = renderer.getActiveCamera();

    if (model.volumeMapper) {
      const range = publicAPI.getSliceRange();
      const bounds = model.volumeMapper.getBounds();

      const clampedSlice = clamp(slice, ...range);

      const center = [
        (bounds[0] + bounds[1]) / 2.0,
        (bounds[2] + bounds[3]) / 2.0,
        (bounds[4] + bounds[5]) / 2.0
      ];

      const distance = camera.getDistance();
      const dop = camera.getDirectionOfProjection();
      vtkMath.normalize(dop);

      const midPoint = (range[1] + range[0]) / 2.0;
      const zeroPoint = [
        center[0] - dop[0] * midPoint,
        center[1] - dop[1] * midPoint,
        center[2] - dop[2] * midPoint
      ];
      const slicePoint = [
        zeroPoint[0] + dop[0] * clampedSlice,
        zeroPoint[1] + dop[1] * clampedSlice,
        zeroPoint[2] + dop[2] * clampedSlice
      ];

      const cameraPos = [
        slicePoint[0] - dop[0] * distance,
        slicePoint[1] - dop[1] * distance,
        slicePoint[2] - dop[2] * distance
      ];

      camera.setPosition(...cameraPos);
      camera.setFocalPoint(...slicePoint);

      // run Callback
      const onScroll = publicAPI.getOnScroll();
      if (onScroll) onScroll(slicePoint);
    }
  };

  publicAPI.getSliceRange = () => {
    if (model.volumeMapper) {
      const sliceNormal = publicAPI.getSliceNormal();

      if (
        sliceNormal[0] === cache.sliceNormal[0] &&
        sliceNormal[1] === cache.sliceNormal[1] &&
        sliceNormal[2] === cache.sliceNormal[2]
      ) {
        return cache.sliceRange;
      }

      const bounds = model.volumeMapper.getBounds();
      const points = boundsToCorners(bounds);

      // Get rotation matrix from normal to +X (since bounds is aligned to XYZ)
      const transform = vtkMatrixBuilder
        .buildFromDegree()
        .identity()
        .rotateFromDirections(sliceNormal, [1, 0, 0]);

      points.forEach(pt => transform.apply(pt));

      // range is now maximum X distance
      let minX = Infinity;
      let maxX = -Infinity;
      for (let i = 0; i < 8; i++) {
        const x = points[i][0];
        if (x > maxX) {
          maxX = x;
        }
        if (x < minX) {
          minX = x;
        }
      }

      cache.sliceNormal = sliceNormal;
      cache.sliceRange = [minX, maxX];
      return cache.sliceRange;
    }
    return [0, 0];
  };

  // Slice normal is just camera DOP
  publicAPI.getSliceNormal = () => {
    if (model.volumeMapper && model.interactor) {
      const renderer = model.interactor.getCurrentRenderer();
      const camera = renderer.getActiveCamera();
      return camera.getDirectionOfProjection();
    }
    return [0, 0, 0];
  };

  // Thought this was a good idea, but no.
  // publicAPI.getSliceNormal = () => cache.sliceNormal;

  /**
   * Move the camera to the given slice normal and viewup direction. Viewup can be used to rotate the display of the image around the direction of view.
   *
   * TODO: setting the slice ALWAYS resets to the volume center, but we need to be able to rotate from an arbitrary position, AKA the intersection of all 3 slice planes.
   */
  // in world space
  publicAPI.setSliceNormal = (normal, viewUp = [0, 1, 0]) => {
    const renderer = model.interactor.getCurrentRenderer();
    const camera = renderer.getActiveCamera();

    // Copy arguments to the model, so they can be GET-ed later
    model.sliceNormal = [...normal];
    model.viewUp = [...viewUp];

    //copy arguments for internal editing so we don't cause sideeffects
    const _normal = [...normal];
    const _viewUp = [...viewUp];

    if (model.volumeMapper) {
      vtkMath.normalize(_normal);
      let mapper = model.volumeMapper;
      // get the mapper if the model is actually the actor, not the mapper
      if (!model.volumeMapper.getInputData && model.volumeMapper.getMapper) {
        mapper = model.volumeMapper.getMapper();
      }
      let volumeCoordinateSpace = vec9toMat3(
        mapper.getInputData().getDirection()
      );
      // Transpose the volume's coordinate space to create a transformation matrix
      vtkMath.transpose3x3(volumeCoordinateSpace, volumeCoordinateSpace);
      // Convert the provided normal into the volume's space
      vtkMath.multiply3x3_vect3(volumeCoordinateSpace, _normal, _normal);

      let center = camera.getFocalPoint();
      let dist = camera.getDistance();
      let angle = camera.getViewAngle();

      if (Number.isNaN(dist) || dist === undefined) {
        // Default the volume center
        const bounds = model.volumeMapper.getBounds();
        // diagonal will be used as "width" of camera scene
        const diagonal = Math.sqrt(
          vtkMath.distance2BetweenPoints(
            [bounds[0], bounds[2], bounds[4]],
            [bounds[1], bounds[3], bounds[5]]
          )
        );

        // center will be used as initial focal point
        center = [
          (bounds[0] + bounds[1]) / 2.0,
          (bounds[2] + bounds[3]) / 2.0,
          (bounds[4] + bounds[5]) / 2.0,
        ];

        angle = 90;

        // distance from camera to focal point
        dist = diagonal / (2 * Math.tan((angle / 360) * Math.PI));
      }

      const cameraPos = [
        center[0] - _normal[0] * dist,
        center[1] - _normal[1] * dist,
        center[2] - _normal[2] * dist
      ];

      // set viewUp based on DOP rotation
      // const oldDop = camera.getDirectionOfProjection();
      // const transform = vtkMatrixBuilder
      //   .buildFromDegree()
      //   .identity()
      //   .rotateFromDirections(oldDop, normal);
      // const viewUp = [0, 1, 0];
      // transform.apply(viewUp);

      vtkMath.multiply3x3_vect3(volumeCoordinateSpace, _viewUp, _viewUp);

      const { slabThickness } = model;

      camera.setPosition(...cameraPos);
      camera.setDistance(dist);
      // should be set after pos and distance
      camera.setDirectionOfProjection(..._normal);
      camera.setViewUp(..._viewUp);
      camera.setViewAngle(angle);
      camera.setClippingRange(
        dist - slabThickness / 2,
        dist + slabThickness / 2
      );

      publicAPI.setCenterOfRotation(center);
    }
  };

  publicAPI.setSlabThickness = slabThickness => {
    model.slabThickness = slabThickness;

    // Update the camera clipping range if the slab
    // thickness property is changed
    const renderer = model.interactor.getCurrentRenderer();
    const camera = renderer.getActiveCamera();
    const dist = camera.getDistance();

    camera.setClippingRange(dist - slabThickness / 2, dist + slabThickness / 2);
  };

  setManipulators();
}

// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------

const DEFAULT_VALUES = {
  slabThickness: 0.1
};

// ----------------------------------------------------------------------------

export function extend(publicAPI, model, initialValues = {}) {
  Object.assign(model, DEFAULT_VALUES, initialValues);

  // Inheritance
  vtkInteractorStyleManipulator.extend(publicAPI, model, initialValues);

  macro.setGet(publicAPI, model, ["volumeMapper", "onScroll"]);
  macro.get(publicAPI, model, ["slabThickness", "viewUp"]);

  // Object specific methods
  vtkInteractorStyleMPRSlice(publicAPI, model);
}

// ----------------------------------------------------------------------------

export const newInstance = macro.newInstance(
  extend,
  "vtkInteractorStyleMPRSlice"
);

// ----------------------------------------------------------------------------

export default Object.assign({ newInstance, extend });

// TODO: work with VTK to change the internal formatting of arrays.
function vec9toMat3(vec9) {
  if (vec9.length !== 9) {
    throw Error("Array not length 9");
  }
  //prettier-ignore
  return [
    [vec9[0], vec9[1], vec9[2]],
    [vec9[3], vec9[4], vec9[5]],
    [vec9[6], vec9[7], vec9[8]],
  ];
}
updateVolumesForRendering(volumes) {
    this.renderer.removeAllVolumes();
    if (volumes && volumes.length) {
      volumes.forEach((volume, i )=> {

        if (!volume.isA("vtkVolume")) {
          console.warn("Data to <Vtk2D> is not vtkVolume data");
        } else {
          // 必须在添加数据卷之后添加,以便它可以在前面呈现
          this.renderer.addVolume(volume);
        }
      });
    }
    this.renderWindow.render();
  }
updateSlicePlane() {
    // TODO: optimize so you don't have to calculate EVERYTHING every time?

    // rotate around the vector of the cross product of the plane and viewup as the X component
    let sliceXRotVector = [];
    vec3.cross(sliceXRotVector, this.sliceViewUp, this.slicePlaneNormal);
    vec3.normalize(sliceXRotVector, sliceXRotVector);

    // rotate the viewUp vector as the Y component
    let sliceYRotVector = this.sliceViewUp;

    const planeMat = mat4.create();
    mat4.rotate(
      planeMat,
      planeMat,
      degrees2radians(this.slicePlaneYRotation),
      sliceYRotVector
    );
    mat4.rotate(
      planeMat,
      planeMat,
      degrees2radians(this.slicePlaneXRotation),
      sliceXRotVector
    );
    vec3.transformMat4(
      this.cachedSlicePlane,
      this.slicePlaneNormal,
      planeMat
    );

    // Rotate the viewUp in 90 degree increments
    const viewRotQuat = quat.create();
    // Use - degrees since the axis of rotation should really be the direction of projection, which is the negative of the plane normal
    quat.setAxisAngle(
      viewRotQuat,
      this.cachedSlicePlane,
      degrees2radians(-this.viewRotation)
    );
    quat.normalize(viewRotQuat, viewRotQuat);

    // rotate the ViewUp with the x and z rotations
    const xQuat = quat.create();
    quat.setAxisAngle(xQuat, sliceXRotVector, degrees2radians(this.slicePlaneXRotation));
    quat.normalize(xQuat, xQuat);
    const viewUpQuat = quat.create();
    quat.add(viewUpQuat, xQuat, viewRotQuat);
    vec3.transformQuat(this.cachedSliceViewUp, this.sliceViewUp, viewRotQuat);

    // update the view's slice
    const renderWindow = this.genericRenderWindow.getRenderWindow();
    const istyle = renderWindow
      .getInteractor()
      .getInteractorStyle()
    if (istyle && istyle.setSliceNormal) {
      istyle.setSliceNormal(this.cachedSlicePlane, this.cachedSliceViewUp);
    }

    renderWindow.render();
  }

 onResize() {  
    // TODO: debounce for performance reasons?
    this.genericRenderWindow.resize();

    const [width, height] = [
      this.$refs.container.offsetWidth,
      this.$refs.container.offsetHeight
    ];

    this.width = width;
    this.height = height;
  }
jourdain commented 3 years ago

@itsRaul did you close the issue because you solved it?

itsRaul commented 3 years ago

@itsRaul did you close the issue because you solved it?

It has not been resolved. After working on different computer equipment, I found that this problem may be related to the computer equipment. Including other companies that use vtk to develop this feature, this problem also occurs

finetjul commented 3 years ago

Do you have an easy-to-reproduce code to share ? (on codepen for example)

limin5156 commented 2 years ago

I also encounter this same problem, have fixed this bug ?

limin5156 commented 2 years ago

@itsRaul