Skip to content

Instantly share code, notes, and snippets.

@jameskane05
Last active August 25, 2020 01:02
Show Gist options
  • Save jameskane05/38d71a7a477e306d358115f33ee59c37 to your computer and use it in GitHub Desktop.
Save jameskane05/38d71a7a477e306d358115f33ee59c37 to your computer and use it in GitHub Desktop.
Cubemap attempt within gltf-model-plus.js
import nextTick from "../utils/next-tick";
import { mapMaterials, convertStandardMaterial } from "../utils/material-utils";
import SketchfabZipWorker from "../workers/sketchfab-zip.worker.js";
import { getCustomGLTFParserURLResolver } from "../utils/media-url-utils";
import { promisifyWorker } from "../utils/promisify-worker.js";
import { MeshBVH, acceleratedRaycast } from "three-mesh-bvh";
import { disposeNode, cloneObject3D } from "../utils/three-utils";
import HubsTextureLoader from "../loaders/HubsTextureLoader";
import HubsBasisTextureLoader from "../loaders/HubsBasisTextureLoader";
import { createDefaultEnvironmentMap } from "./environment-map";
import lavaTile from "../assets/images/lavatile.jpg";
THREE.Mesh.prototype.raycast = acceleratedRaycast;
class GLTFCache {
cache = new Map();
set(src, gltf) {
this.cache.set(src, {
gltf,
count: 0
});
return this.retain(src);
}
has(src) {
return this.cache.has(src);
}
get(src) {
return this.cache.get(src);
}
retain(src) {
const cacheItem = this.cache.get(src);
cacheItem.count++;
return cacheItem;
}
release(src) {
const cacheItem = this.cache.get(src);
if (!cacheItem) {
console.error(`Releasing uncached gltf ${src}`);
return;
}
cacheItem.count--;
if (cacheItem.count <= 0) {
cacheItem.gltf.scene.traverse(disposeNode);
this.cache.delete(src);
}
}
}
const gltfCache = new GLTFCache();
const inflightGltfs = new Map();
const extractZipFile = promisifyWorker(new SketchfabZipWorker());
function defaultInflator(el, componentName, componentData) {
if (!AFRAME.components[componentName]) {
throw new Error(`Inflator failed. "${componentName}" component does not exist.`);
}
if (AFRAME.components[componentName].multiple && Array.isArray(componentData)) {
for (let i = 0; i < componentData.length; i++) {
el.setAttribute(componentName + "__" + i, componentData[i]);
}
} else {
el.setAttribute(componentName, componentData);
}
}
AFRAME.GLTFModelPlus = {
// eslint-disable-next-line no-unused-vars
components: {},
registerComponent(componentKey, componentName, inflator) {
inflator = inflator || defaultInflator;
AFRAME.GLTFModelPlus.components[componentKey] = { inflator, componentName };
}
};
function generateMeshBVH(object3D) {
object3D.traverse(obj => {
// note that we might already have a bounds tree if this was a clone of an object with one
const hasBufferGeometry = obj.isMesh && obj.geometry.isBufferGeometry;
const hasBoundsTree = hasBufferGeometry && obj.geometry.boundsTree;
if (hasBufferGeometry && !hasBoundsTree && obj.geometry.attributes.position) {
const geo = obj.geometry;
const triCount = geo.index ? geo.index.count / 3 : geo.attributes.position.count / 3;
// only bother using memory and time making a BVH if there are a reasonable number of tris,
// and if there are too many it's too painful and large to tolerate doing it (at least until
// we put this in a web worker)
if (triCount > 1000 && triCount < 1000000) {
// note that bounds tree construction creates an index as a side effect if one doesn't already exist
geo.boundsTree = new MeshBVH(obj.geometry, { strategy: 0, maxDepth: 30 });
}
}
});
}
function cloneGltf(gltf) {
return {
animations: gltf.scene.animations,
scene: cloneObject3D(gltf.scene)
};
}
function getHubsComponents(node) {
const hubsComponents =
node.userData.gltfExtensions &&
(node.userData.gltfExtensions.MOZ_hubs_components || node.userData.gltfExtensions.HUBS_components);
// We can remove support for legacy components when our environment, avatar and interactable models are
// updated to match Spoke output.
const legacyComponents = node.userData.components;
return hubsComponents || legacyComponents;
}
/// Walks the tree of three.js objects starting at the given node, using the GLTF data
/// and template data to construct A-Frame entities and components when necessary.
/// (It's unnecessary to construct entities for subtrees that have no component data
/// or templates associated with any of their nodes.)
///
/// Returns the A-Frame entity associated with the given node, if one was constructed.
const inflateEntities = function(indexToEntityMap, node, templates, isRoot, modelToWorldScale = 1) {
// TODO: Remove this once we update the legacy avatars to the new node names
if (node.name === "Chest") {
node.name = "Spine";
} else if (node.name === "Root Scene") {
node.name = "AvatarRoot";
} else if (node.name === "Bot_Skinned") {
node.name = "AvatarMesh";
}
// inflate subtrees first so that we can determine whether or not this node needs to be inflated
const childEntities = [];
const children = node.children.slice(0); // setObject3D mutates the node's parent, so we have to copy
for (const child of children) {
const el = inflateEntities(indexToEntityMap, child, templates);
if (el) {
childEntities.push(el);
}
}
const entityComponents = getHubsComponents(node);
const nodeHasBehavior = !!entityComponents || node.name in templates;
if (!nodeHasBehavior && !childEntities.length && !isRoot) {
return null; // we don't need an entity for this node
}
const el = document.createElement("a-entity");
el.append.apply(el, childEntities);
// Remove invalid CSS class name characters.
const className = (node.name || node.uuid).replace(/[^\w-]/g, "");
el.classList.add(className);
// AFRAME rotation component expects rotations in YXZ, convert it
if (node.rotation.order !== "YXZ") {
node.rotation.setFromQuaternion(node.quaternion, "YXZ");
}
// Copy over the object's transform to the THREE.Group and reset the actual transform of the Object3D
// all updates to the object should be done through the THREE.Group wrapper
el.object3D.position.copy(node.position);
el.object3D.rotation.copy(node.rotation);
el.object3D.scale.copy(node.scale).multiplyScalar(modelToWorldScale);
el.object3D.matrixNeedsUpdate = true;
node.matrixAutoUpdate = false;
node.matrix.identity();
node.matrix.decompose(node.position, node.rotation, node.scale);
el.setObject3D(node.type.toLowerCase(), node);
if (entityComponents && "nav-mesh" in entityComponents) {
el.setObject3D("mesh", node);
}
// Set the name of the `THREE.Group` to match the name of the node,
// so that templates can be attached to the correct AFrame entity.
el.object3D.name = node.name;
// Set the uuid of the `THREE.Group` to match the uuid of the node,
// so that `THREE.PropertyBinding` will find (and later animate)
// the group. See `PropertyBinding.findNode`:
// https://github.com/mrdoob/three.js/blob/dev/src/animation/PropertyBinding.js#L211
el.object3D.uuid = node.uuid;
node.uuid = THREE.Math.generateUUID();
if (node.animations) {
// Pass animations up to the group object so that when we can pass the group as
// the optional root in `THREE.AnimationMixer.clipAction` and use the hierarchy
// preserved under the group (but not the node). Otherwise `clipArray` will be
// `null` in `THREE.AnimationClip.findByName`.
node.parent.animations = node.animations;
}
if (node.morphTargetInfluences) {
node.parent.morphTargetInfluences = node.morphTargetInfluences;
}
const gltfIndex = node.userData.gltfIndex;
if (gltfIndex !== undefined) {
indexToEntityMap[gltfIndex] = el;
}
return el;
};
async function inflateComponents(inflatedEntity, indexToEntityMap) {
let isFirstInflation = true;
const objectInflations = [];
inflatedEntity.object3D.traverse(async object3D => {
const objectInflation = {};
objectInflation.promise = new Promise(resolve => (objectInflation.resolve = resolve));
objectInflations.push(objectInflation);
if (!isFirstInflation) {
await objectInflations.shift().promise;
}
isFirstInflation = false;
const entityComponents = getHubsComponents(object3D);
const el = object3D.el;
if (entityComponents && el) {
for (const prop in entityComponents) {
if (entityComponents.hasOwnProperty(prop) && AFRAME.GLTFModelPlus.components.hasOwnProperty(prop)) {
const { componentName, inflator } = AFRAME.GLTFModelPlus.components[prop];
await inflator(el, componentName, entityComponents[prop], entityComponents, indexToEntityMap);
}
}
}
objectInflation.resolve();
});
await objectInflations.shift().promise;
}
function attachTemplate(root, name, templateRoot) {
const targetEls = root.querySelectorAll("." + name);
for (const el of targetEls) {
const root = templateRoot.cloneNode(true);
// Merge root element attributes with the target element
for (const { name, value } of root.attributes) {
el.setAttribute(name, value);
}
// Append all child elements
while (root.children.length > 0) {
el.appendChild(root.children[0]);
}
}
}
function getHubsComponentsExtension(node) {
if (node.extensions && node.extensions.MOZ_hubs_components) {
return node.extensions.MOZ_hubs_components;
} else if (node.extensions && node.extensions.HUBS_components) {
return node.extensions.HUBS_components;
} else if (node.extras && node.extras.gltfExtensions && node.extras.gltfExtensions.MOZ_hubs_components) {
return node.extras.gltfExtensions.MOZ_hubs_components;
}
}
// Versions are documented here: https://github.com/mozilla/hubs/wiki/MOZ_hubs_components-Changelog
// Make sure to update the wiki and Spoke when bumping a version
function runMigration(version, json) {
if (version < 2) {
//old heightfields will be on the same node as the nav-mesh, delete those
const oldHeightfieldNode = json.nodes.find(node => {
const components = getHubsComponentsExtension(node);
return components && components.heightfield && components["nav-mesh"];
});
if (oldHeightfieldNode) {
if (oldHeightfieldNode.extensions && oldHeightfieldNode.extensions.MOZ_hubs_components) {
delete oldHeightfieldNode.extensions.MOZ_hubs_components.heightfield;
} else if (oldHeightfieldNode.extensions && oldHeightfieldNode.extensions.HUBS_components) {
delete oldHeightfieldNode.extensions.HUBS_components.heightfield;
} else if (
oldHeightfieldNode.extras &&
oldHeightfieldNode.extras.gltfExtensions &&
oldHeightfieldNode.extras.gltfExtensions.MOZ_hubs_components
) {
delete oldHeightfieldNode.extras.gltfExtensions.MOZ_hubs_components;
}
}
}
if (version < 4) {
// Lights prior to version 4 should treat range === 0 as if it has zero decay
if (json.nodes) {
for (const node of json.nodes) {
const components = getHubsComponentsExtension(node);
if (!components) {
continue;
}
const light = components["spot-light"] || components["point-light"];
if (light && light.range === 0) {
light.decay = 0;
}
}
}
}
}
const loadLightmap = async (parser, materialIndex) => {
const lightmapDef = parser.json.materials[materialIndex].extensions.MOZ_lightmap;
const [material, lightMap] = await Promise.all([
parser.getDependency("material", materialIndex),
parser.getDependency("texture", lightmapDef.index)
]);
material.lightMap = lightMap;
material.lightMapIntensity = lightmapDef.intensity !== undefined ? lightmapDef.intensity : 1;
return lightMap;
};
export async function loadGLTF(src, contentType, onProgress, jsonPreprocessor) {
let gltfUrl = src;
let fileMap;
if (contentType && (contentType.includes("model/gltf+zip") || contentType.includes("application/x-zip-compressed"))) {
fileMap = await extractZipFile(gltfUrl);
gltfUrl = fileMap["scene.gtlf"];
}
const loadingManager = new THREE.LoadingManager();
loadingManager.setURLModifier(getCustomGLTFParserURLResolver(gltfUrl));
const gltfLoader = new THREE.GLTFLoader(loadingManager);
gltfLoader.setBasisTextureLoader(new HubsBasisTextureLoader(loadingManager));
const parser = await new Promise((resolve, reject) => gltfLoader.createParser(gltfUrl, resolve, onProgress, reject));
parser.textureLoader = new HubsTextureLoader(loadingManager);
if (jsonPreprocessor) {
parser.json = jsonPreprocessor(parser.json);
}
let version = 0;
if (
parser.json.extensions &&
parser.json.extensions.MOZ_hubs_components &&
parser.json.extensions.MOZ_hubs_components.hasOwnProperty("version")
) {
version = parser.json.extensions.MOZ_hubs_components.version;
}
runMigration(version, parser.json);
const nodes = parser.json.nodes;
if (nodes) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (!node.extras) {
node.extras = {};
}
node.extras.gltfIndex = i;
}
}
// Mark the special nodes/meshes in json for efficient parse, all json manipulation should happen before this point
parser.markDefs();
const materials = parser.json.materials;
const extensionDeps = [];
if (materials) {
for (let i = 0; i < materials.length; i++) {
const materialNode = materials[i];
if (!materialNode.extensions) continue;
if (materialNode.extensions.MOZ_lightmap) {
extensionDeps.push(loadLightmap(parser, i));
}
}
}
// Note this is being done in place of parser.parse() which we now no longer call. This gives us more control over the order of execution.
const [scenes, animations, cameras] = await Promise.all([
parser.getDependencies("scene"),
parser.getDependencies("animation"),
parser.getDependencies("camera"),
Promise.all(extensionDeps)
]);
const gltf = {
scene: scenes[parser.json.scene || 0],
scenes,
animations,
cameras,
asset: parser.json.asset,
parser,
userData: {}
};
// this is likely a noop since the whole parser will get GCed
parser.cache.removeAll();
gltf.scene.traverse(object => {
// GLTFLoader sets matrixAutoUpdate on animated objects, we want to keep the defaults
object.matrixAutoUpdate = THREE.Object3D.DefaultMatrixAutoUpdate;
const materialQuality = window.APP.store.materialQualitySetting;
object.material = mapMaterials(object, material => convertStandardMaterial(material, materialQuality));
});
if (fileMap) {
// The GLTF is now cached as a THREE object, we can get rid of the original blobs
Object.keys(fileMap).forEach(URL.revokeObjectURL);
}
gltf.scene.animations = gltf.animations;
return gltf;
}
export async function loadModel(src, contentType = null, useCache = false, jsonPreprocessor = null) {
if (useCache) {
if (gltfCache.has(src)) {
gltfCache.retain(src);
return cloneGltf(gltfCache.get(src).gltf);
} else {
if (inflightGltfs.has(src)) {
const gltf = await inflightGltfs.get(src);
gltfCache.retain(src);
return cloneGltf(gltf);
} else {
const promise = loadGLTF(src, contentType, null, jsonPreprocessor);
inflightGltfs.set(src, promise);
const gltf = await promise;
inflightGltfs.delete(src);
gltfCache.set(src, gltf);
return cloneGltf(gltf);
}
}
} else {
return loadGLTF(src, contentType, null, jsonPreprocessor);
}
}
function resolveAsset(src) {
// If the src attribute is a selector, get the url from the asset item.
if (src && src.charAt(0) === "#") {
const assetEl = document.getElementById(src.substring(1));
if (assetEl) {
return assetEl.getAttribute("src");
}
}
return src;
}
/**
* Loads a GLTF model, optionally recursively "inflates" the child nodes of a model into a-entities and sets
* allowed components on them if defined in the node's extras.
* @namespace gltf
* @component gltf-model-plus
*/
AFRAME.registerComponent("gltf-model-plus", {
schema: {
src: { type: "string" },
contentType: { type: "string" },
useCache: { default: true },
inflate: { default: false },
batch: { default: false },
modelToWorldScale: { type: "number", default: 1 }
},
init() {
// This can be set externally if a consumer wants to do some node preprocssing.
this.jsonPreprocessor = null;
this.loadTemplates();
},
update() {
this.applySrc(resolveAsset(this.data.src), this.data.contentType);
},
remove() {
if (this.data.batch && this.model) {
this.el.sceneEl.systems["hubs-systems"].batchManagerSystem.removeObject(this.el.object3DMap.mesh);
}
const src = resolveAsset(this.data.src);
if (src) {
gltfCache.release(src);
}
},
loadTemplates() {
this.templates = {};
this.el.querySelectorAll(":scope > template").forEach(templateEl => {
const root = document.importNode(templateEl.firstElementChild || templateEl.content.firstElementChild, true);
this.templates[templateEl.getAttribute("data-name")] = root;
});
},
async applySrc(src, contentType) {
try {
if (src === this.lastSrc) return;
const lastSrc = this.lastSrc;
this.lastSrc = src;
if (!src) {
if (this.inflatedEl) {
console.warn("gltf-model-plus set to an empty source, unloading inflated model.");
this.disposeLastInflatedEl();
}
return;
}
this.el.emit("model-loading");
const gltf = await loadModel(src, contentType, this.data.useCache, this.jsonPreprocessor);
// If we started loading something else already
// TODO: there should be a way to cancel loading instead
if (src != this.lastSrc) return;
// If we had inflated something already before, clean that up
this.disposeLastInflatedEl();
this.model = gltf.scene || gltf.scenes[0];
if (this.data.batch) {
this.el.sceneEl.systems["hubs-systems"].batchManagerSystem.addObject(this.model);
}
if (gltf.animations.length > 0) {
this.el.setAttribute("animation-mixer", {});
this.el.components["animation-mixer"].initMixer(this.model.animations);
} else {
generateMeshBVH(this.model);
}
const indexToEntityMap = {};
let object3DToSet = this.model;
if (
this.data.inflate &&
(this.inflatedEl = inflateEntities(
indexToEntityMap,
this.model,
this.templates,
true,
this.data.modelToWorldScale
))
) {
this.el.appendChild(this.inflatedEl);
object3DToSet = this.inflatedEl.object3D;
object3DToSet.visible = false;
// TODO: Still don't fully understand the lifecycle here and how it differs between browsers, we should dig in more
// Wait one tick for the appended custom elements to be connected before attaching templates
await nextTick();
if (src != this.lastSrc) return; // TODO: there must be a nicer pattern for this
await inflateComponents(this.inflatedEl, indexToEntityMap);
for (const name in this.templates) {
attachTemplate(this.el, name, this.templates[name]);
}
}
// The call to setObject3D below recursively clobbers any `el` backreferences to entities
// in the entire inflated entity graph to point to `object3DToSet`.
//
// We don't want those overwritten, since lots of code assumes `object3d.el` points to the relevant
// A-Frame entity for that three.js object, so we back them up and re-wire them here. If we didn't do
// this, all the `el` properties on these object3ds would point to the `object3DToSet` which is either
// the model or the root GLTF inflated entity.
const rewires = [];
object3DToSet.traverse(o => {
const el = o.el;
if (el) rewires.push(() => (o.el = el));
});
const environmentMapComponent = this.el.sceneEl.components["environment-map"];
if (environmentMapComponent) {
environmentMapComponent.applyEnvironmentMap(object3DToSet);
}
if (lastSrc) {
gltfCache.release(lastSrc);
}
this.el.setObject3D("mesh", object3DToSet);
rewires.forEach(f => f());
object3DToSet.visible = true;
if (object3DToSet.name == "Cubemap Test") {
const cubeCamera = new THREE.CubeCamera(0.1, 1000, 512);
cubeCamera.renderTarget.texture.generateMipmaps = true;
cubeCamera.renderTarget.texture.minFilter = THREE.LinearMipmapLinearFilter;
cubeCamera.renderTarget.texture.mapping = THREE.CubeReflectionMapping;
cubeCamera.position.set(0, 5, 0); // Camera must be positioned in the center of the room you want to reflect
// TODO: position the cubeCamera dynamically based on the object3D
this.el.sceneEl.object3D.add(cubeCamera);
const loader = new THREE.TextureLoader();
const rMap = loader.load(lavaTile);
rMap.wrapS = THREE.RepeatWrapping;
rMap.wrapT = THREE.RepeatWrapping;
rMap.repeat.set(2, 2);
const boxProjectedMat = new THREE.MeshPhysicalMaterial({
color: new THREE.Color("#ffffff"),
roughness: .5,
roughnessMap: rMap,
envMap: cubeCamera.renderTarget.texture
});
// I have no idea why, but this line makes or breaks the effect
const defEnvMap = await createDefaultEnvironmentMap();
boxProjectedMat.onBeforeCompile = (shader) => {
// TODO: set uniforms dynamically depending on object3D size
shader.uniforms.cubeMapSize = { value: new THREE.Vector3(10, 10, 10) };
shader.uniforms.cubeMapPos = { value: new THREE.Vector3(0, 5, 0) };
shader.uniforms.flipEnvMap.value = true;
// replace shader chunks with box projection chunks
shader.vertexShader = "varying vec3 vWorldPosition;\n" + shader.vertexShader;
shader.vertexShader = shader.vertexShader.replace(`#include <worldpos_vertex>`, worldposReplace);
shader.fragmentShader = shader.fragmentShader.replace(`#include <envmap_physical_pars_fragment>`, envmapPhysicalParsReplace);
};
// TODO: apply the boxProjectedMat to either an existing mesh in the scene or else dynamically size this, too
const groundPlane = new THREE.Mesh(new THREE.PlaneBufferGeometry(10, 10), boxProjectedMat);
groundPlane.rotateX(-Math.PI / 2);
groundPlane.position.set(0, 0.01, 0);
this.el.sceneEl.object3D.add(groundPlane);
cubeCamera.update(this.el.sceneEl.renderer, this.el.sceneEl.object3D);
this.el.sceneEl.render();
}
this.el.emit("model-loaded", { format: "gltf", model: object3DToSet });
} catch (e) {
gltfCache.release(src);
console.error("Failed to load glTF model", e, this);
this.el.emit("model-error", { format: "gltf", src });
}
},
disposeLastInflatedEl() {
if (this.inflatedEl) {
this.inflatedEl.parentNode.removeChild(this.inflatedEl);
this.inflatedEl.object3D.traverse(x => {
if (x.material && x.material.dispose) {
x.material.dispose();
}
if (x.geometry) {
if (x.geometry.dispose) {
x.geometry.dispose();
}
x.geometry.boundsTree = null;
}
});
delete this.inflatedEl;
this.el.removeAttribute("animation-mixer");
}
}
});
// shader injection for box projected cube environment mapping
const worldposReplace = /* glsl */ `
#define BOX_PROJECTED_ENV_MAP
#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP )
vec4 worldPosition = modelMatrix * vec4( transformed, 1.0 );
#ifdef BOX_PROJECTED_ENV_MAP
vWorldPosition = worldPosition.xyz;
#endif
#endif
`;
const envmapPhysicalParsReplace = /* glsl */ `
#if defined( USE_ENVMAP )
#define BOX_PROJECTED_ENV_MAP
#ifdef BOX_PROJECTED_ENV_MAP
uniform vec3 cubeMapSize;
uniform vec3 cubeMapPos;
varying vec3 vWorldPosition;
vec3 parallaxCorrectNormal( vec3 v, vec3 cubeSize, vec3 cubePos ) {
vec3 nDir = normalize( v );
vec3 rbmax = ( .5 * cubeSize + cubePos - vWorldPosition ) / nDir;
vec3 rbmin = ( -.5 * cubeSize + cubePos - vWorldPosition ) / nDir;
vec3 rbminmax;
rbminmax.x = ( nDir.x > 0. ) ? rbmax.x : rbmin.x;
rbminmax.y = ( nDir.y > 0. ) ? rbmax.y : rbmin.y;
rbminmax.z = ( nDir.z > 0. ) ? rbmax.z : rbmin.z;
float correction = min( min( rbminmax.x, rbminmax.y ), rbminmax.z );
vec3 boxIntersection = vWorldPosition + nDir * correction;
return boxIntersection - cubePos;
}
#endif
#ifdef ENVMAP_MODE_REFRACTION
uniform float refractionRatio;
#endif
vec3 getLightProbeIndirectIrradiance( /*const in SpecularLightProbe specularLightProbe,*/ const in GeometricContext geometry, const in int maxMIPLevel ) {
vec3 worldNormal = inverseTransformDirection( geometry.normal, viewMatrix );
#ifdef ENVMAP_TYPE_CUBE
#ifdef BOX_PROJECTED_ENV_MAP
worldNormal = parallaxCorrectNormal( worldNormal, cubeMapSize, cubeMapPos );
#endif
vec3 queryVec = vec3( flipEnvMap * worldNormal.x, worldNormal.yz );
// TODO: replace with properly filtered cubemaps and access the irradiance LOD level, be it the last LOD level
// of a specular cubemap, or just the default level of a specially created irradiance cubemap.
#ifdef TEXTURE_LOD_EXT
vec4 envMapColor = textureCubeLodEXT( envMap, queryVec, float( maxMIPLevel ) );
#else
// force the bias high to get the last LOD level as it is the most blurred.
vec4 envMapColor = textureCube( envMap, queryVec, float( maxMIPLevel ) );
#endif
envMapColor.rgb = envMapTexelToLinear( envMapColor ).rgb;
#elif defined( ENVMAP_TYPE_CUBE_UV )
vec4 envMapColor = textureCubeUV( envMap, worldNormal, 1.0 );
#else
vec4 envMapColor = vec4( 0.0 );
#endif
return PI * envMapColor.rgb * envMapIntensity;
}
// Trowbridge-Reitz distribution to Mip level, following the logic of http://casual-effects.blogspot.ca/2011/08/plausible-environment-lighting-in-two.html
float getSpecularMIPLevel( const in float roughness, const in int maxMIPLevel ) {
float maxMIPLevelScalar = float( maxMIPLevel );
float sigma = PI * roughness * roughness / ( 1.0 + roughness );
float desiredMIPLevel = maxMIPLevelScalar + log2( sigma );
// clamp to allowable LOD ranges.
return clamp( desiredMIPLevel, 0.0, maxMIPLevelScalar );
}
vec3 getLightProbeIndirectRadiance( /*const in SpecularLightProbe specularLightProbe,*/ const in vec3 viewDir, const in vec3 normal, const in float roughness, const in int maxMIPLevel ) {
#ifdef ENVMAP_MODE_REFLECTION
vec3 reflectVec = reflect( -viewDir, normal );
// Mixing the reflection with the normal is more accurate and keeps rough objects from gathering light from behind their tangent plane.
reflectVec = normalize( mix( reflectVec, normal, roughness * roughness) );
#else
vec3 reflectVec = refract( -viewDir, normal, refractionRatio );
#endif
reflectVec = inverseTransformDirection( reflectVec, viewMatrix );
float specularMIPLevel = getSpecularMIPLevel( roughness, maxMIPLevel );
#ifdef ENVMAP_TYPE_CUBE
#ifdef BOX_PROJECTED_ENV_MAP
reflectVec = parallaxCorrectNormal( reflectVec, cubeMapSize, cubeMapPos );
#endif
vec3 queryReflectVec = vec3( flipEnvMap * reflectVec.x, reflectVec.yz );
#ifdef TEXTURE_LOD_EXT
vec4 envMapColor = textureCubeLodEXT( envMap, queryReflectVec, specularMIPLevel );
#else
vec4 envMapColor = textureCube( envMap, queryReflectVec, specularMIPLevel );
#endif
envMapColor.rgb = envMapTexelToLinear( envMapColor ).rgb;
#elif defined( ENVMAP_TYPE_CUBE_UV )
vec4 envMapColor = textureCubeUV( envMap, reflectVec, roughness );
#endif
return envMapColor.rgb * envMapIntensity;
}
#endif
`;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment