a Cessna airplane, using 3745 nodes and 3897 faces with orders from 3 up to 32.
the cessna.obj
file was sourced from this collection
a fork of the bl.ock 3D Wireframe from OBJ File from @brianedcoffey
README.md
This is a simple example of parsing the obj
file format for 3D objects, and the displaying 3D objects in svg, using orthographic projection from 3D to 2D, and using d3's Zoom Behavior for zoom (mouse scroll or swipe), rotation (mouse drag) and pan (spacebar plus mouse drag). Line opacity indicates of visual depth (lines closer to the viewer are more opaque than lines further away).
The obj
file format is a commonly used file format for 3D model exchange, and the teapot is a common example - in this case, the obj file was sourced from this collection.
WebGL (e.g. via threejs) is becoming the most common approach for browser-based visualization of 3D models, but there are situations where svg display is still desirable. The use of line opacity for depth is a nice simple trick that works well in some cases but not all - more cases would benefit from a dynamic z-ordering of surfaces (by redrawing surfaces only when necessary), which would then allow for opaque surface rendering instead of just wireframes. I plan to demonstrate this in an upcoming example.
forked from micahstubbs's block: 3D wireframe interaction with d3 zoom
xxxxxxxxxx
<meta charset="utf-8">
<style>
.polygon {
fill: none;
stroke: #000;
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src='https://npmcdn.com/babel-core@5.8.34/browser.min.js'></script>
<script src="obj_parse.js"></script>
<script lang='babel' type='text/babel'>
const width = 960;
const height = 500;
const margin = 20;
const squareLength = d3.min([width, height]) - 2 * margin;
const xScale = d3.scale.linear()
.range([width / 2 - squareLength / 2, width / 2 + squareLength / 2]);
const yScale = d3.scale.linear()
.range([height / 2 + squareLength / 2, height / 2 - squareLength / 2]);
const zScale = d3.scale.linear()
.range([0, 1]);
const camera = {
inclination: Math.PI / 2,
azimuth: 0,
center: {
x: 0,
y: 0,
z: 0
}
};
let panMode = false;
let surfaces;
let obj;
const zoom = d3.behavior.zoom()
.scaleExtent([1, 10])
.on('zoom', zoomed); // eslint-disable-line
const svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.call(zoom);
const container = svg.append('g');
function translatePT(pt, center) {
return {
x: pt.x - center.x,
y: pt.y - center.y,
z: pt.z - center.z
};
}
function rotatePT(pt, camera) { // eslint-disable-line
// first rotate around y-axis to the azimuth angle
const xp2 = pt.x * Math.cos(camera.azimuth) - pt.z * Math.sin(camera.azimuth);
const zp2 = pt.x * Math.sin(camera.azimuth) + pt.z * Math.cos(camera.azimuth);
// then around the x axis to pi/2 minus the inclination angle
const a = Math.PI / 2 - camera.inclination;
const zp3 = zp2 * Math.cos(a) - pt.y * Math.sin(a);
const yp3 = zp2 * Math.sin(a) + pt.y * Math.cos(a);
return { x: xp2, y: yp3, z: zp3 };
}
function project_orthographic(surfaces, camera) { // eslint-disable-line
surfaces.forEach(points => {
points.forEach(point => {
const pointT = translatePT(point, camera.center);
const pointR = rotatePT(pointT, camera);
point.px = pointR.x;
point.py = pointR.y;
point.pz = pointR.z;
});
});
return surfaces;
}
function draw(surfaces) { // eslint-disable-line
const polygons = container.selectAll('.polygon')
.data(surfaces);
polygons.enter().append('path')
.attr('class', 'polygon');
polygons
.attr('d', datum => {
const d = datum.map(point => [xScale(point.px), yScale(point.py)]);
return `M${d.join('L')}Z`;
})
.attr('opacity', datum => {
const d = datum.map(point => point.pz);
return zScale(d3.max(d));
});
}
function update(surfaces, camera) { // eslint-disable-line
surfaces = project_orthographic(surfaces, camera);
draw(surfaces);
}
let mousePanX = 0;
let mousePanY = 0;
let mouseRotX = 0;
let mouseRotY = 0;
let prevMouseX = 0;
let prevMouseY = 0;
let prevScale = 1;
function zoomed() {
const deltaX = d3.event.translate[0] - prevMouseX;
const deltaY = d3.event.translate[1] - prevMouseY;
prevMouseX = d3.event.translate[0];
prevMouseY = d3.event.translate[1];
if (d3.event.scale === prevScale) {
if (panMode) {
mousePanX += deltaX;
mousePanY += deltaY;
container.attr('transform', `'translate(${mousePanX}, ${mousePanY})`);
} else {
mouseRotX += deltaX;
mouseRotY += deltaY;
camera.inclination = Math.PI / 2 + mouseRotY / 500;
camera.azimuth = -1 * mouseRotX / 500;
}
} else {
xScale
.range([width / 2 - d3.event.scale * squareLength / 2,
width / 2 + d3.event.scale * squareLength / 2]);
yScale
.range([height / 2 + d3.event.scale * squareLength / 2,
height / 2 - d3.event.scale * squareLength / 2]);
prevScale = d3.event.scale;
}
// console.log('camera', camera);
// console.log('xScale.range()', xScale.range());
// console.log('yScale.range()', yScale.range());
update(surfaces, camera);
}
function checkKeyDown(e) {
const event = window.event ? window.event : e;
if (event.keyCode === 32) { panMode = true; }
}
function checkKeyUp(e) {
const event = window.event ? window.event : e;
if (event.keyCode === 32) { panMode = false; }
}
document.onkeydown = checkKeyDown;
document.onkeyup = checkKeyUp;
// let surfaces;
// let obj;
d3.text('cessna.obj', (error, objFileText) => {
if (error) throw error;
obj = parse_obj_text(objFileText); // eslint-disable-line
surfaces = obj.surfaces;
const extreme = d3.max([Math.abs(obj.extents[0]), Math.abs(obj.extents[1])]);
// set a custom starting position
// by setting the properties of the camera object
camera.center = {
x: 1.0351724999999998,
y: 0,
z: 0
};
camera.azimuth = -1.046;
camera.inclination = 1.9627963267948965;
xScale.domain([-extreme, extreme]);
yScale.domain([-extreme, extreme]);
zScale.domain([-extreme, extreme]);
// manually set the range of the
// xScale and yScale as well
xScale.range([25.05443972783297, 934.9455602721671]);
yScale.range([704.9455602721671, -204.94556027216703]);
update(surfaces, camera);
});
</script>
https://d3js.org/d3.v3.min.js
https://npmcdn.com/babel-core@5.8.34/browser.min.js