top of page

Visualizing Well Trajectories in 3D with Minimum Curvature Methods and Three.js

  • Writer: Khiem Nguyen
    Khiem Nguyen
  • Jul 10, 2024
  • 6 min read

Introduction

In the well drilling, accurately visualizing well trajectories is crucial for planning and operational efficiency. In this article, we'll explore how to use Three.js to create a 3D visualization of oil well trajectories. This approach leverages the Minimum Curvature Method to ensure the visual representation is accurate and informative.


Setting Up the Environment


Prerequisites

Before we dive into the code, ensure you have the following installed:

  • Bun (a modern JavaScript runtime)

  • A code editor like VSCode


Initial Setup

Create a new project directory and initialize it with Bun:

mkdir 3d-trajectory
cd 3d-trajectory
bun init

Libraries Used

  • Three.js: A powerful library for rendering 3D graphics in the browser.

  • CSS2DRenderer: Renders 2D elements in the 3D scene.

  • Line2: For rendering thick lines in Three.js.

  • OrbitControls: Enables interactive camera controls.


Project Structure

/project-root
  ├── src
  │   ├── main.js           // Main entry point
  │   ├── threejs-setup.js  // Three.js setup and utility functions
  │   ├── calculations.js   // Wellbore trajectory calculations
  ├── index.html            // HTML file with import map
  └── styles.css            // Optional CSS for styling

Setting Up HTML and Importing Three.js Libraries

To get started with our 3D visualization, we'll first set up the HTML structure and import the necessary Three.js libraries.

Create an index.html file with the following content:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>3D Borehole Trajectory Visualization</title>
  <link rel="stylesheet" href="css/styles.css">
</head>
<body>
  <script type="importmap">
    {
      "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.module.js",
        "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/"
      }
    }
  </script>
  <script type="module" src="js/main.js"></script>
</body>
</html>

Setting Up the CSS

Ensure the CSS styles as style.css are set up to provide a clean display for the 3D visualization.

body {
  margin: 0;
  font-family: Arial, sans-serif;
}

canvas {
  display: block;
}

Setting Up the 3D Scene

First, we set up a basic 3D scene with a perspective camera and a renderer in threejs-setup.js

import * as THREE from 'three';
import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js';

function initThreeJS() {
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 5000);
  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  const labelRenderer = new CSS2DRenderer();
  labelRenderer.setSize(window.innerWidth, window.innerHeight);
  labelRenderer.domElement.style.position = 'absolute';
  labelRenderer.domElement.style.top = '0';
  labelRenderer.domElement.style.pointerEvents = 'none';
  renderer.domElement.parentElement.appendChild(labelRenderer.domElement);

  return { scene, camera, renderer, labelRenderer };
}

Creating the Compass

Next, we'll add a compass that will display in the upper left corner to help visualize the orientation of the 3D scene in threejs-setup.js

function createCompass() {
  const compassScene = new THREE.Scene();
  const compassSize = 150;
  const frustumSize = 500;
  const compassCamera = new THREE.OrthographicCamera(
    frustumSize / -2,
    frustumSize / 2,
    frustumSize / 2,
    frustumSize / -2,
    0.1,
    1000
  );
  compassCamera.position.set(0, 200, 200);
  compassCamera.lookAt(0, 0, 0);
  compassScene.add(compassCamera);

  const compassRenderer = new THREE.WebGLRenderer({ antialias: true });
  compassRenderer.setSize(compassSize, compassSize);
  compassRenderer.domElement.style.position = 'absolute';
  compassRenderer.domElement.style.top = '10px';
  compassRenderer.domElement.style.left = '10px';
  document.body.appendChild(compassRenderer.domElement);

  const compassLabelRenderer = new CSS2DRenderer();
  compassLabelRenderer.setSize(compassSize, compassSize);
  compassLabelRenderer.domElement.style.position = 'absolute';
  compassLabelRenderer.domElement.style.top = '10px';
  compassLabelRenderer.domElement.style.left = '10px';
  compassLabelRenderer.domElement.style.pointerEvents = 'none';
  document.body.appendChild(compassLabelRenderer.domElement);

  // Add compass axes
  const axesMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
  const axesGeometry = new THREE.BufferGeometry().setFromPoints([
    new THREE.Vector3(0, 10, 0),
    new THREE.Vector3(-100, 10, 0),
    new THREE.Vector3(0, 10, 0),
    new THREE.Vector3(100, 10, 0),
    new THREE.Vector3(0, 10, 0),
    new THREE.Vector3(0, 10, -100),
    new THREE.Vector3(0, 10, 0),
    new THREE.Vector3(0, 10, 100),
  ]);
  const axes = new THREE.LineSegments(axesGeometry, axesMaterial);
  compassScene.add(axes);

  return { compassScene, compassCamera, compassRenderer, compassLabelRenderer };
}

Adding Orbit Controls

Now, let's add orbit controls to allow interactive navigation of the 3D scene in threejs-setup.js

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

function addOrbitControls(camera, renderer) {
  const controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 0.25;
  controls.screenSpacePanning = false;
  controls.minDistance = 10;
  controls.maxDistance = 5000;
  controls.maxPolarAngle = Math.PI / 2;

  camera.position.set(0, 100, 200);
  controls.update();

  return controls;
}

Creating the Ground Plane

A ground plane helps to provide a reference for the 3D objects and improve the perception of depth in threejs-setup.js

function createGroundPlane(scene) {
  const planeGeometry = new THREE.PlaneGeometry(2000, 2000);
  const planeMaterial = new THREE.MeshBasicMaterial({ color: 0xaaaaaa, side: THREE.DoubleSide });
  const plane = new THREE.Mesh(planeGeometry, planeMaterial);
  plane.rotation.x = -Math.PI / 2;
  scene.add(plane);
}

Minimum Curvature Calculations

The Minimum Curvature Method is essential for accurately calculating wellbore trajectories. This method ensures that the visual representation of the well path is smooth and continuous. Let's dive into the code required to perform these calculations.

Functions for Minimum Curvature Calculations

First, in calculations.js, we need to define the necessary functions for the calculations:

function toRadians(degrees) {
  return degrees * (Math.PI / 180);
}

function calculateDL(azimuth1, azimuth2, inclination1, inclination2) {
  const deltaAzimuth = toRadians(azimuth2 - azimuth1);
  const deltaInclination = toRadians(inclination2 - inclination1);
  return Math.sqrt(deltaAzimuth * deltaAzimuth + deltaInclination * deltaInclination);
}

function calculateRF(dl) {
  return dl === 0 ? 1 : 2 * (Math.sin(dl / 2) / dl);
}

function parseValue(value) {
  return parseFloat(value.toString().replace(/,/g, ''));
}

function calculateNorth(incl1, incl2, azm1, azm2, deltaMD, rf, initialNorth) {
  return initialNorth + (deltaMD / 2) * (Math.sin(incl1) * Math.cos(azm1) + Math.sin(incl2) * Math.cos(azm2)) * rf;
}

function calculateEast(incl1, incl2, azm1, azm2, deltaMD, rf, initialEast) {
  return initialEast + (deltaMD / 2) * (Math.sin(incl1) * Math.sin(azm1) + Math.sin(incl2) * Math.sin(azm2)) * rf;
}

function calculateTVD(incl1, incl2, deltaMD, rf, initialTVD) {
  return initialTVD + (deltaMD / 2) * (Math.cos(incl1) + Math.cos(incl2)) * rf;
}

Main Calculation Function

Next, we define the main function that will calculate the coordinates based on the survey points:

function calculateCoordinates(point1, point2, initialCoordinates) {
  const depth1 = parseValue(point1.measuredDepth);
  const depth2 = parseValue(point2.measuredDepth);
  const azimuth1 = parseValue(point1.azimuth);
  const azimuth2 = parseValue(point2.azimuth);
  const inclination1 = parseValue(point1.inclination);
  const inclination2 = parseValue(point2.inclination);
  const deltaMD = depth2 - depth1;

  if (deltaMD === 0) {
    console.warn('Delta MD is zero, skipping calculation');
    return initialCoordinates;
  }

  const dl = calculateDL(azimuth1, azimuth2, inclination1, inclination2);
  if (isNaN(dl)) {
    console.error('DL calculation resulted in NaN', { point1, point2, dl });
    return initialCoordinates;
  }

  const rf = calculateRF(dl);
  if (isNaN(rf)) {
    console.error('RF calculation resulted in NaN', { dl, rf });
    return initialCoordinates;
  }

  const incl1 = toRadians(inclination1);
  const incl2 = toRadians(inclination2);
  const azm1 = toRadians(azimuth1);
  const azm2 = toRadians(azimuth2);

  const north = calculateNorth(incl1, incl2, azm1, azm2, deltaMD, rf, initialCoordinates.north);
  const east = calculateEast(incl1, incl2, azm1, azm2, deltaMD, rf, initialCoordinates.east);
  const tvd = calculateTVD(incl1, incl2, deltaMD, rf, initialCoordinates.tvd);

  if (isNaN(north) || isNaN(east) || isNaN(tvd)) {
    console.error('Coordinate calculation resulted in NaN', { north, east, tvd, point1, point2, initialCoordinates });
    return initialCoordinates;
  }

  return { north, east, tvd };
}

Compute Survey Coordinates

Finally, we use these functions to compute the coordinates for the entire set of survey points:

function computeSurveyCoordinates(surveyPoints) {
  const initialCoordinates = { north: 0, east: 0, tvd: 0 };
  const coordinates = [initialCoordinates];
  for (let i = 1; i < surveyPoints.length; i++) {
    const newCoordinates = calculateCoordinates(surveyPoints[i - 1], surveyPoints[i], coordinates[i - 1]);
    coordinates.push(newCoordinates);
  }
  console.log(coordinates);
  return coordinates;
}

export { calculateDogleg, calculateRatioFactor, calculateNorth, calculateEast, calculateTVD, calculateCoordinates, computeSurveyCoordinates };

These calculations ensure that the well trajectory is accurately plotted in 3D space, providing a clear and informative visualization of the well path.


Finally Combine It All

main.js will initialize the Three.js scene, set up the compass, add orbit controls, and compute and visualize the well trajectory.

import * as THREE from 'three';
import { surveyPoints } from './survey-points.js';
import { computeSurveyCoordinates } from './calculations.js';
import { initThreeJS, createLine, createMarker, createCompass, createGroundPlane, fitCameraToObject } from './threejs-setup.js';

document.addEventListener('DOMContentLoaded', async () => {
  const { scene, camera, renderer, controls, labelRenderer } = initThreeJS();
  console.log('Scene, camera, renderer, controls, and labelRenderer initialized');

  const coordinates = computeSurveyCoordinates(surveyPoints);

  const line = createLine(coordinates);
  scene.add(line);
  console.log('Line created and added to scene:', line);

  // Fit the camera to the line
  fitCameraToObject(camera, line, 1.75);
  console.log('Camera fitted to object');

  const marker = createMarker(scene);
  console.log('Marker created and added to scene:', marker);

  // Create and add compass
  const { compassScene, compassCamera, compassRenderer, compassLabelRenderer } = createCompass();
  console.log('Compass created and added to scene');

  // Create and add ground plane
  createGroundPlane(scene);
  console.log('Ground plane created and added to scene');

  function animate() {
    requestAnimationFrame(animate);
    controls.update(); // only required if controls.enableDamping = true, or if controls.autoRotate = true
    renderer.render(scene, camera);
    labelRenderer.render(scene, camera);

    // Copy the main camera's rotation to the compass camera
    compassCamera.quaternion.copy(camera.quaternion);

    // Reset compass camera position to keep it at a fixed distance
    const distance = 200; // Fixed distance from the origin
    compassCamera.position.set(
      distance * Math.sin(camera.rotation.y),
      distance,
      distance * Math.cos(camera.rotation.x)
    );
    compassCamera.lookAt(compassScene.position);

    compassRenderer.render(compassScene, compassCamera);
    compassLabelRenderer.render(compassScene, compassCamera);
  }
  animate();
  console.log('Rendering loop started');
});

Integration Using Sample Trajectory Data

You can try with sample data (spiral curve as shown in the image) through the file below. You can copy and paste into the project under the name survey-points.js


Conclusion

By leveraging the Minimum Curvature Method and Three.js, we can create accurate and visually compelling 3D representations of oil well trajectories. This visualization aids in better planning and operational efficiency in the Well Drilling. Through careful setup of the environment, HTML, and CSS, and by implementing the necessary mathematical calculations, we can achieve precise and interactive visualizations.

For a complete source code and detailed setup, you can visit the GitHub repository here 👇:

Comentários


© 2024

bottom of page