UNPKG

threepipe

Version:

A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.

219 lines (213 loc) 10.3 kB
import { _testFinish, BaseGroundPlugin, BasicShadowMap, DataUtils, DirectionalLight, LoadingScreenPlugin, ProgressivePlugin, ShaderChunk, shaderReplaceString, SSAAPlugin, ThreeViewer, Vector3, } from 'threepipe'; import { TweakpaneUiPlugin } from '@threepipe/plugin-tweakpane'; const hdris = [ 'https://threejs.org/examples/textures/equirectangular/quarry_01_1k.hdr', 'https://threejs.org/examples/textures/equirectangular/spot1Lux.hdr', 'https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr', 'https://dist.pixotronics.com/webgi/assets/hdr/gem_2.hdr', 'https://hdrihaven.r2cache.com/hdr/1k/studio_small_04_1k.hdr', 'https://hdrihaven.r2cache.com/hdr/1k/studio_small_03_1k.hdr', 'https://threejs.org/examples/textures/equirectangular/pedestrian_overpass_1k.hdr', 'https://threejs.org/examples/textures/equirectangular/blouberg_sunrise_2_1k.hdr', 'https://threejs.org/examples/textures/equirectangular/royal_esplanade_1k.hdr', 'https://threejs.org/examples/textures/equirectangular/moonless_golf_1k.hdr', 'https://threejs.org/examples/textures/equirectangular/san_giuseppe_bridge_2k.hdr', 'https://hdrihaven.r2cache.com/hdr/1k/studio_small_06_1k.hdr', 'https://hdrihaven.r2cache.com/hdr/1k/studio_small_05_1k.hdr', 'https://hdrihaven.r2cache.com/hdr/1k/studio_small_02_1k.hdr', 'https://hdrihaven.r2cache.com/hdr/1k/studio_small_01_1k.hdr', 'https://hdrihaven.r2cache.com/hdr/1k/empty_warehouse_01_1k.hdr', ]; async function init() { const viewer = new ThreeViewer({ canvas: document.getElementById('mcanvas'), msaa: false, rgbm: false, plugins: [new ProgressivePlugin(window.TESTING ? 20 : 200), SSAAPlugin, LoadingScreenPlugin], dropzone: { addOptions: { disposeSceneObjects: true, autoSetEnvironment: true, autoSetBackground: true, }, }, }); const directionalLight = createDirLight(viewer); viewer.materialManager.registerMaterialExtension(extension); viewer.renderManager.renderer.shadowMap.type = BasicShadowMap; // extra check to ignore the sampling of shadow if intensity is 0 ShaderChunk.lights_fragment_begin = shaderReplaceString(ShaderChunk.lights_fragment_begin, 'directLight.color *= ( directLight.visible && receiveShadow )', 'directLight.color *= ( directLight.visible && receiveShadow && length(directLight.color) > 0.001)', { replaceAll: true }); const ground = viewer.addPluginSync(BaseGroundPlugin); ground.mesh.castShadow = false; ground.material.roughness = 1; ground.material.metalness = 0; const ui = viewer.addPluginSync(new TweakpaneUiPlugin(false)); await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { autoCenter: true, autoScale: true, }); viewer.scene.envMapIntensity = 1; await viewer.setEnvironmentMap(hdris[0], { setBackground: true, }); ui.appendChild({ type: 'dropdown', label: 'Environment Map', children: hdris.map((url) => ({ label: url.split('/').pop().split('.').shift(), value: url, })), value: hdris[0], onChange: async (ev) => { console.log(ev.value); await viewer.setEnvironmentMap(ev.value, { setBackground: true, }); refreshHist(); }, }); let histogram2 = createHistogramFromImage(viewer.scene.environment?.image); function refreshHist() { histogram2 = createHistogramFromImage(viewer.scene.environment?.image); } viewer.addEventListener('postFrame', () => updateLight(viewer, directionalLight, histogram2)); ui.setupPluginUi(BaseGroundPlugin); // const targetPreview = viewer.addPluginSync(new RenderTargetPreviewPlugin()) // targetPreview.addTarget(()=>directionalLight.shadow.map, 'shadow') } const extension = { isCompatible: () => true, computeCacheKey: () => 'aomap1', shaderExtender(shader) { shader.fragmentShader = shaderReplaceString(shader.fragmentShader, '#include <aomap_fragment>', ` #ifdef USE_AOMAP // reads channel R, compatible with a combined OcclusionRoughnessMetallic (RGB) texture float ambientOcclusion = ( texture2D( aoMap, vAoMapUv ).r - 1.0 ) * aoMapIntensity + 1.0; #else const int ii = 0; DirectionalLightShadow edls = directionalLightShadows[ ii ]; float ambientOcclusion = getShadow( directionalShadowMap[ ii ], edls.shadowMapSize, edls.shadowBias, edls.shadowRadius, vDirectionalShadowCoord[ ii ] ); #endif reflectedLight.indirectDiffuse *= ambientOcclusion; #if defined( USE_ENVMAP ) && defined( STANDARD ) float dotNV = saturate( dot( geometry.normal, geometry.viewDir ) ); reflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness ); #endif `); // shader.defines.USE_UV = '' }, }; function createDirLight(viewer) { const directionalLight = new DirectionalLight(0xffffff, 4); directionalLight.position.set(-2, -2, 2); directionalLight.lookAt(0, 0, 0); directionalLight.color.set(0xffffff); directionalLight.intensity = 0; directionalLight.castShadow = true; directionalLight.shadow.mapSize.setScalar(1024); directionalLight.shadow.camera.near = 0.1; directionalLight.shadow.camera.far = 10; directionalLight.shadow.camera.top = 2; directionalLight.shadow.camera.bottom = -2; directionalLight.shadow.camera.left = -2; directionalLight.shadow.camera.right = 2; viewer.scene.addObject(directionalLight, { addToRoot: true }); // move to index 0 in parent.children, so that directionalLight always has index 0 in shader. required for material extension const parent = directionalLight.parent; const index = parent.children.indexOf(directionalLight); if (index > 0) { parent.children.splice(index, 1); parent.children.unshift(directionalLight); } return directionalLight; } function updateLight(viewer, directionalLight, histogram) { if (viewer.renderManager.frameCount < 1) return; // if (viewer.renderManager.frameCount > 2) return const bounds = viewer.scene.getBounds(false); const size = bounds.getSize(new Vector3()).length(); const center = bounds.getCenter(new Vector3()); const i = viewer.renderManager.frameCount <= 1 ? histogram.brightestI : histogram.sampleIndex(); histogram.indexToColor(i, directionalLight); directionalLight.intensity = 0; // so it doesnt show in the scene histogram.indexToPosition(i, directionalLight.position).multiplyScalar(0.5 + size).add(center); directionalLight.lookAt(center); directionalLight.shadow.camera.near = Math.max(size / 100, 0.1); directionalLight.shadow.camera.far = size * 2.5; directionalLight.shadow.camera.updateProjectionMatrix(); viewer.renderManager.resetShadows(); } function sampleRandom2(pow = 2) { return Math.max(0, Math.pow(Math.random(), pow) - 0.001); } function sampleRandom() { return Math.max(0, Math.random() - 0.001); } const maxIntensityClamp = 50; const ignoreBottomBins = 1; // should be at-least 1 to ignore black pixels. const numBins = 100; // Number of bins in the histogram (configurable) const sampleRandPower = 1.25; // increase this to give more focus to higher intensity pixels. between 1 and 2 const topHalf = true; // todo if this is true, half the shadow in shader? function createHistogramFromImage(image) { const histogram = []; let maxIntensity = -1; let brightestI = 0; // const maxIntensity1 = 65504 for (let i = 0; i < image.data.length / 4; i++) { const r = DataUtils.fromHalfFloat(image.data[i * 4]); const g = DataUtils.fromHalfFloat(image.data[i * 4 + 1]); const b = DataUtils.fromHalfFloat(image.data[i * 4 + 2]); const a = DataUtils.fromHalfFloat(image.data[i * 4 + 3]); const intensity = a * Math.max(r, g, b); // Calculate intensity const binIndex = Math.floor(numBins * Math.max(0, Math.min(1 - 0.001, intensity / maxIntensityClamp))); // Calculate the bin index histogram[binIndex] || (histogram[binIndex] = []); histogram[binIndex].push(i); if (maxIntensity < intensity) { maxIntensity = intensity; brightestI = i; } if (topHalf && i > image.data.length / 8) break; } histogram.reverse(); const cdf = histogram.map((bin) => bin ? bin.length : 0); const maxW = numBins - 1 - ignoreBottomBins + 1; cdf[0] = cdf[0] * maxW; for (let i = 1; i < numBins; i++) { cdf[i] = cdf[i - 1] + (cdf[i] || 0) * (maxW - i); // *i for intensity of that bin } console.log(cdf); return { histogram, cdf, brightestI, maxIntensity, sampleIndex: () => { const max = cdf[cdf.length - 1]; const r = sampleRandom2(sampleRandPower) * max; const binIndex = cdf.findIndex((value) => value >= r); const bin = histogram[binIndex]; const index = Math.floor(bin.length * sampleRandom()); return bin[index]; }, indexToPosition: (i, position) => { // todo handle envMapRotation const { width, height } = image; const x = i % width / width; const y = 1 - Math.floor(i / width) / height; const phi = Math.PI * (x * 2 - 1); const theta = Math.PI * 0.5 * (y * 2 - 1); return position.set(Math.cos(theta) * Math.cos(phi), Math.sin(theta), Math.cos(theta) * Math.sin(phi)); }, indexToColor: (i, light) => { // todo handle envMapIntensity const r = DataUtils.fromHalfFloat(image.data[i * 4]); const g = DataUtils.fromHalfFloat(image.data[i * 4 + 1]); const b = DataUtils.fromHalfFloat(image.data[i * 4 + 2]); const a = DataUtils.fromHalfFloat(image.data[i * 4 + 3]); light.color.setRGB(Math.min(1, r * a), Math.min(1, g * a), Math.min(1, b * a)); light.intensity = Math.min(a * Math.max(r, g, b), maxIntensityClamp); }, }; } init().finally(_testFinish);