This example a simple parsing of the obj file format for 3D objects, and the display of 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 is indicative 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.
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="obj_parse.js"></script>
<script>
var width = 960,
height = 500,
margin = 20;
var square_length = d3.min([width,height]) - 2 * margin;
var x_scale = d3.scale.linear()
.range([width/2 - square_length/2, width/2 + square_length/2]);
var y_scale = d3.scale.linear()
.range([height/2 + square_length/2, height/2 - square_length/2]);
var z_scale = d3.scale.linear()
.range([0, 1]);
var camera = {inclination: Math.PI/2, azimuth: 0, center: {x:0, y:0, z:0}};
var pan_mode = false;
var zoom = d3.behavior.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
var container = svg.append("g");
function translate_pt(pt, center) {
return {x: pt.x - center.x,
y: pt.y - center.y,
z: pt.z - center.z};
}
function rotate_pt(pt, camera) {
// first rotate around y-axis to the azimuth angle
var xp2 = pt.x * Math.cos(camera.azimuth) - pt.z * Math.sin(camera.azimuth);
var 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
var a = Math.PI/2 - camera.inclination;
var zp3 = zp2 * Math.cos(a) - pt.y * Math.sin(a);
var yp3 = zp2 * Math.sin(a) + pt.y * Math.cos(a);
return {x: xp2, y: yp3, z: zp3};
}
function project_orthographic(surfaces, camera) {
surfaces.forEach(function(points){
points.forEach(function(point){
var point_t = translate_pt(point, camera.center);
var point_r = rotate_pt(point_t, camera);
point.px = point_r.x;
point.py = point_r.y;
point.pz = point_r.z;
});
});
return surfaces;
}
function draw(surfaces) {
var polygons = container.selectAll(".polygon")
.data(surfaces);
polygons.enter().append("path")
.attr("class", "polygon");
polygons
.attr("d", function(datum) {
var d = datum.map(function(point){
return [x_scale(point.px), y_scale(point.py)];
});
return "M" + d.join("L") + "Z";
})
.attr("opacity", function(datum) {
var d = datum.map(function(point){ return point.pz; });
return z_scale(d3.max(d));
});
}
function update(surfaces, camera) {
surfaces = project_orthographic(surfaces, camera);
draw(surfaces);
}
var mouse_pan_x = 0,
mouse_pan_y = 0,
mouse_rot_x = 0,
mouse_rot_y = 0,
prev_mouse_x = 0,
prev_mouse_y = 0
prev_scale = 1;
function zoomed() {
var delta_x = d3.event.translate[0] - prev_mouse_x,
delta_y = d3.event.translate[1] - prev_mouse_y;
prev_mouse_x = d3.event.translate[0];
prev_mouse_y = d3.event.translate[1];
if ( d3.event.scale === prev_scale ) {
if (pan_mode) {
mouse_pan_x += delta_x;
mouse_pan_y += delta_y;
container.attr("transform",
"translate(" + mouse_pan_x + "," + mouse_pan_y + ")");
} else {
mouse_rot_x += delta_x;
mouse_rot_y += delta_y;
camera.inclination = Math.PI/2 + mouse_rot_y / 500;
camera.azimuth = -1 * mouse_rot_x / 500;
}
} else {
x_scale
.range([width/2 - d3.event.scale * square_length/2,
width/2 + d3.event.scale * square_length/2]);
y_scale
.range([height/2 + d3.event.scale * square_length/2,
height/2 - d3.event.scale * square_length/2]);
prev_scale = d3.event.scale;
}
update(surfaces, camera);
}
function checkKeyDown(e) {
var event = window.event ? window.event : e;
if (event.keyCode === 32) { pan_mode = true; }
}
function checkKeyUp(e) {
var event = window.event ? window.event : e;
if (event.keyCode === 32) { pan_mode = false; }
}
document.onkeydown = checkKeyDown;
document.onkeyup = checkKeyUp;
var surfaces;
var obj;
d3.text("teapot.obj", function(error, obj_file_text) {
if (error) throw error;
obj = parse_obj_text(obj_file_text);
surfaces = obj.surfaces;
var extreme = d3.max([Math.abs(obj.extents[0]),Math.abs(obj.extents[1])]);
camera.center = obj.center;
x_scale.domain([-extreme, extreme]);
y_scale.domain([-extreme, extreme]);
z_scale.domain([-extreme, extreme]);
update(surfaces, camera)
});
</script>
https://d3js.org/d3.v3.min.js