prophetk-3d-viewer
Version:
A Three.js r128 based GLB/GLTF model viewer for WeChat Mini Program and Web.
115 lines (107 loc) • 4.58 kB
JavaScript
const THREE = require('../vendor/three-r128.js');
const VERTEX_SHADER = `
varying vec3 vGridWorldPosition;
void main() {
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
vGridWorldPosition = worldPosition.xyz;
gl_Position = projectionMatrix * viewMatrix * worldPosition;
}
`;
const FRAGMENT_SHADER = `
uniform vec3 gridColor;
uniform float gridSpacing;
uniform float pixelAngle;
uniform vec3 viewRight;
uniform vec3 viewUp;
uniform vec3 viewForward;
varying vec3 vGridWorldPosition;
float gridLines(float spacing) {
vec2 coordinate = vGridWorldPosition.xz / spacing;
vec2 width;
#if GRID_DERIVATIVES
width = fwidth(coordinate);
#else
// WebGL 1 fallback for devices without OES_standard_derivatives.
vec3 offset = vGridWorldPosition - cameraPosition;
float depth = max(dot(offset, viewForward), 0.0001);
float heightDelta = abs(offset.y) < 0.00001 ? 0.00001 : offset.y;
vec3 dx = viewRight * (depth * pixelAngle);
vec3 dy = viewUp * (depth * pixelAngle);
// Analytic screen-to-horizontal-plane derivatives. Unlike an isotropic
// distance estimate, these keep the two line directions equally thin.
dx -= offset * (dx.y / heightDelta);
dy -= offset * (dy.y / heightDelta);
width = (abs(dx.xz) + abs(dy.xz)) / spacing;
#endif
width = max(width, vec2(0.00001));
vec2 distanceToLine = abs(fract(coordinate - 0.5) - 0.5);
vec2 lines = 1.0 - smoothstep(width * 0.35, width * 1.35, distanceToLine);
// Fade each family of lines before it becomes subpixel and aliases near
// the horizon. This avoids a visible model-sized rectangular boundary.
lines *= 1.0 - smoothstep(vec2(0.25), vec2(0.5), width);
return max(lines.x, lines.y);
}
void main() {
float alpha = max(gridLines(gridSpacing) * 0.22, gridLines(gridSpacing * 5.0) * 0.34);
if (alpha < 0.001) discard;
gl_FragColor = vec4(gridColor, alpha);
}
`;
// A two-triangle horizontal surface covering the whole camera frustum. The
// repeating pattern uses world coordinates, so resizing/recentering this
// surface never moves the grid lines or ties their extent to model size.
class DragPlaneGrid extends THREE.Mesh {
constructor(height, spacing, derivatives) {
const geometry = new THREE.PlaneGeometry(1, 1);
geometry.rotateX(-Math.PI / 2);
const material = new THREE.ShaderMaterial({
vertexShader: VERTEX_SHADER,
fragmentShader: FRAGMENT_SHADER,
defines: { GRID_DERIVATIVES: derivatives ? 1 : 0 },
extensions: { derivatives: !!derivatives },
uniforms: {
gridColor: { value: new THREE.Color(0x80cfff) },
gridSpacing: { value: spacing },
pixelAngle: { value: 0.001 },
viewRight: { value: new THREE.Vector3(1, 0, 0) },
viewUp: { value: new THREE.Vector3(0, 1, 0) },
viewForward: { value: new THREE.Vector3(0, 0, -1) },
},
transparent: true,
depthWrite: false,
depthTest: false,
side: THREE.DoubleSide,
toneMapped: false,
});
super(geometry, material);
this.position.y = height;
this.frustumCulled = false;
this.raycast = () => {};
this._coverage = new THREE.Box3();
this._corner = new THREE.Vector3();
}
update(camera, viewportHeight) {
camera.updateMatrixWorld(true);
this._coverage.makeEmpty();
// All points of the visible plane lie inside these world X/Z bounds,
// including when top-of-screen rays miss the plane across the horizon.
for (const x of [-1, 1]) {
for (const y of [-1, 1]) {
for (const z of [-1, 1]) {
this._coverage.expandByPoint(this._corner.set(x, y, z).unproject(camera));
}
}
}
const { min, max } = this._coverage;
const padding = this.material.uniforms.gridSpacing.value * 2;
this.position.x = (min.x + max.x) / 2;
this.position.z = (min.z + max.z) / 2;
this.scale.set(max.x - min.x + padding, 1, max.z - min.z + padding);
this.material.uniforms.pixelAngle.value =
2 * Math.tan(camera.fov * Math.PI / 360) / (camera.zoom * Math.max(1, viewportHeight));
this.material.uniforms.viewRight.value.setFromMatrixColumn(camera.matrixWorld, 0);
this.material.uniforms.viewUp.value.setFromMatrixColumn(camera.matrixWorld, 1);
this.material.uniforms.viewForward.value.setFromMatrixColumn(camera.matrixWorld, 2).negate();
}
}
module.exports = DragPlaneGrid;