phoria2 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/dist/bloom.js +4 -4
- package/dist/bloom.js.map +1 -1
- package/dist/charts.js +436 -436
- package/dist/charts.js.map +1 -1
- package/dist/core.js +1240 -862
- package/dist/core.js.map +1 -1
- package/dist/debug.js +297 -0
- package/dist/debug.js.map +1 -0
- package/dist/edit.js +40 -40
- package/dist/edit.js.map +1 -1
- package/dist/explain.js +32 -13
- package/dist/explain.js.map +1 -1
- package/dist/gltf.js +128 -128
- package/dist/gltf.js.map +1 -1
- package/dist/keyboard.js +19 -19
- package/dist/keyboard.js.map +1 -1
- package/dist/motion.js +35 -35
- package/dist/motion.js.map +1 -1
- package/dist/particles.js +36 -36
- package/dist/particles.js.map +1 -1
- package/dist/passes.js +116 -104
- package/dist/passes.js.map +1 -1
- package/dist/phoria2.js +1 -1
- package/dist/picking.js +90 -71
- package/dist/picking.js.map +1 -1
- package/dist/snapshot.js +1 -1
- package/dist/tooltip.js +1 -1
- package/dist/types/View.d.ts +16 -0
- package/dist/types/debug.d.ts +74 -0
- package/dist/types/explain.d.ts +4 -0
- package/dist/types/frame.d.ts +5 -0
- package/dist/types/material/Material.d.ts +52 -0
- package/dist/types/watch.d.ts +8 -1
- package/dist/types/webgl/hooks.d.ts +48 -0
- package/dist/types/webgl/passes.d.ts +4 -0
- package/dist/types/webgl/programs.d.ts +15 -5
- package/dist/types/webgl/shaders.d.ts +36 -5
- package/llms-full.txt +227 -0
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/schema/scene.schema.json +27 -0
- package/src/View.js +105 -7
- package/src/debug.js +283 -0
- package/src/explain.js +19 -0
- package/src/frame.js +7 -2
- package/src/material/Material.js +121 -2
- package/src/scene/drawables.js +21 -4
- package/src/watch.js +18 -2
- package/src/webgl/WebGLRenderer.js +19 -2
- package/src/webgl/hooks.js +121 -0
- package/src/webgl/passes.js +17 -2
- package/src/webgl/picking.js +23 -2
- package/src/webgl/programs.js +20 -9
- package/src/webgl/resources.js +1 -1
- package/src/webgl/shaders.js +443 -35
|
@@ -26,14 +26,45 @@ export declare const COMMON = "\nvec3 srgbToLinear(vec3 c) {\n return mix(c / 1
|
|
|
26
26
|
export declare const EQUIRECT = "\nvec2 equirectUv(vec3 d) {\n return vec2(atan(d.z, d.x) * 0.15915494 + 0.5, acos(clamp(d.y, -1.0, 1.0)) * 0.31830989);\n}\nvec3 equirectDirection(vec2 uv) {\n float phi = (uv.x - 0.5) * 6.28318531, theta = uv.y * 3.14159265;\n return vec3(cos(phi) * sin(theta), cos(theta), sin(phi) * sin(theta));\n}\n";
|
|
27
27
|
export declare const FULLSCREEN_VERTEX = "\nout vec2 vUv;\nvoid main() {\n vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n vUv = p;\n gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}\n";
|
|
28
28
|
export declare const TONE_MAP = "\nuniform float uExposure;\nuniform int uToneMapping; // 0 none, 1 aces, 2 neutral\n\nvec3 toneMap(vec3 c) {\n c *= uExposure;\n if (uToneMapping == 1) {\n // ACES filmic, Narkowicz's fit\n c = clamp((c * (2.51 * c + 0.03)) / (c * (2.43 * c + 0.59) + 0.14), 0.0, 1.0);\n } else if (uToneMapping == 2) {\n // Khronos PBR Neutral\n float x = min(c.r, min(c.g, c.b));\n float offset = x < 0.08 ? x - 6.25 * x * x : 0.04;\n c -= offset;\n float peak = max(c.r, max(c.g, c.b));\n if (peak > 0.8) {\n float d = 0.2;\n float newPeak = 1.0 - d * d / (peak + d - 0.8);\n c *= newPeak / peak;\n float g = 1.0 - 1.0 / (0.15 * (peak - newPeak) + 1.0);\n c = mix(c, vec3(newPeak), g);\n }\n }\n return c;\n}\n";
|
|
29
|
-
export declare const MESH_VERTEX = "\nlayout(location = 0) in vec3 position;\nlayout(location = 1) in vec3 normal;\nlayout(location = 2) in vec2 uv;\nlayout(location = 3) in vec3 color;\n#ifdef INSTANCED\nlayout(location = 4) in mat4 instanceMatrix;\nlayout(location = 8) in vec3 instanceColor;\n#endif\n#ifdef ANISOTROPY\nlayout(location = 9) in vec4 tangent;\nout vec4 vTangent;\n#endif\n#ifdef UV2\nlayout(location = 10) in vec2 uv2;\nout vec2 vUv2;\n#endif\n\nuniform mat4 uModel;\nuniform mat3 uNormalMatrix;\nuniform mat4 uViewProjection;\nuniform int uHighlightIndex; // -2 none, -1 the whole node, otherwise the instance\n#ifdef FOG\nuniform mat4 uView;\nout float vFogDepth;\n#endif\n\nout vec3 vWorldPosition;\nout vec3 vNormal;\nout vec2 vUv;\nout vec3 vColor;\nflat out float vHighlight;\n#ifdef TRIPLANAR\n// the mesh's own space, so projected maps stay on it as it moves; and how its normals turn into the world\nout vec3 vObjectPosition;\nout vec3 vObjectNormal;\nout mat3 vObjectToWorld;\n#endif\n#ifdef PICK\nflat out uint vPickElement; // which instance, point or segment, for GPU picking\n#endif\n\n\nvec3 srgbToLinear(vec3 c) {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n}\nvec3 linearToSrgb(vec3 c) {\n c = clamp(c, 0.0, 1.0);\n return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\n\n// Matrices times vectors only - a matrix times a matrix per vertex is four times the work, and\n// with 100,000 instances the vertex stage is most of the frame.\nvoid main() {\n vColor = vec3(1.0);\n vHighlight = uHighlightIndex == -1 ? 1.0 : 0.0;\n#ifdef INSTANCED\n vec4
|
|
30
|
-
export declare const POINTS_VERTEX = "\nlayout(location = 0) in vec3 position;\nlayout(location = 3) in vec3 color;\n#ifdef SPRITES\n// each point a square: one instance a point, drawn as a strip over the corners (along 0-1, across -1-1)\nlayout(location = 4) in vec2 corner;\nlayout(location = 11) in float pointSize;\nlayout(location = 12) in float pointOpacity;\nlayout(location = 13) in float pointAngle;\nlayout(location = 14) in float pointIndex;\nuniform vec2 uResolution; // drawing buffer size in pixels\nout vec2 vUv;\nout float vOpacity;\n#endif\n\nuniform mat4 uModel;\nuniform mat4 uView;\nuniform mat4 uProjection;\nuniform float uSize; // pixels (with the pixel ratio applied), or world units when attenuated\nuniform float uScale; // projection focal term x half the buffer height: world size -> pixels at depth 1\nuniform int uHighlightIndex; // -2 none, -1 every point, otherwise the point\n\nout vec3 vColor;\nflat out float vHighlight;\n#ifdef PICK\nflat out uint vPickElement; // which instance, point or segment, for GPU picking\n#endif\n#ifdef FOG\nout float vFogDepth;\n#endif\n\n\nvec3 srgbToLinear(vec3 c) {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n}\nvec3 linearToSrgb(vec3 c) {\n c = clamp(c, 0.0, 1.0);\n return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\n\nvoid main() {\n vec4 viewPosition = uView * (uModel * vec4(
|
|
31
|
-
export declare const LINES_VERTEX = "\nlayout(location = 0) in vec3 segmentStart;\nlayout(location = 1) in vec3 segmentEnd;\nlayout(location = 2) in vec3 segmentColorStart;\nlayout(location = 3) in vec3 segmentColorEnd;\n// the distance along its chain (segments joined end to start) where it starts, in the model's\n// units; 1 if it starts a chain; 1 if it ends one\nlayout(location = 5) in vec3 segmentInfo;\n\nuniform mat4 uModel;\nuniform mat4 uView;\nuniform mat4 uProjection;\nuniform vec2 uResolution; // drawing buffer size in pixels\nuniform float uWidth; // pixels, already scaled by the pixel ratio\nuniform float uNear;\nuniform int uArrows; // arrowheads: 1 at the start of each chain, 2 at its end, 3 at both\nuniform float uArrowSize; // their length in pixels, scaled by the pixel ratio\n\n// six vertices an instance make the segment's quad, as two triangles - x: 0 at its start, 1 at its\n// end; y: -1 or 1 across - and six more its two arrowheads, drawn only when asked for\nconst vec2 QUAD[6] = vec2[6](vec2(0.0, -1.0), vec2(0.0, 1.0), vec2(1.0, -1.0), vec2(1.0, -1.0), vec2(0.0, 1.0), vec2(1.0, 1.0));\n\nout vec3 vColor;\n#
|
|
29
|
+
export declare const MESH_VERTEX = "\nlayout(location = 0) in vec3 position;\nlayout(location = 1) in vec3 normal;\nlayout(location = 2) in vec2 uv;\nlayout(location = 3) in vec3 color;\n#ifdef INSTANCED\nlayout(location = 4) in mat4 instanceMatrix;\nlayout(location = 8) in vec3 instanceColor;\n#endif\n#ifdef ANISOTROPY\nlayout(location = 9) in vec4 tangent;\nout vec4 vTangent;\n#endif\n#ifdef UV2\nlayout(location = 10) in vec2 uv2;\nout vec2 vUv2;\n#endif\n\nuniform mat4 uModel;\nuniform mat3 uNormalMatrix;\nuniform mat4 uViewProjection;\nuniform int uHighlightIndex; // -2 none, -1 the whole node, otherwise the instance\n#ifdef FOG\nuniform mat4 uView;\nout float vFogDepth;\n#endif\n//@hook uniforms\n\nout vec3 vWorldPosition;\nout vec3 vNormal;\nout vec2 vUv;\nout vec3 vColor;\nflat out float vHighlight;\n#ifdef TRIPLANAR\n// the mesh's own space, so projected maps stay on it as it moves; and how its normals turn into the world\nout vec3 vObjectPosition;\nout vec3 vObjectNormal;\nout mat3 vObjectToWorld;\n#endif\n#ifdef PICK\nflat out uint vPickElement; // which instance, point or segment, for GPU picking\n#endif\n\n\nvec3 srgbToLinear(vec3 c) {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n}\nvec3 linearToSrgb(vec3 c) {\n c = clamp(c, 0.0, 1.0);\n return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\n\n// Matrices times vectors only - a matrix times a matrix per vertex is four times the work, and\n// with 100,000 instances the vertex stage is most of the frame.\nvoid main() {\n vColor = vec3(1.0);\n vHighlight = uHighlightIndex == -1 ? 1.0 : 0.0;\n // the vertex in the geometry's own space - where a shader hook moves it\n vec3 local = position;\n vec3 n = normal;\n#ifdef HOOK_VERTEX\n {\n int index = gl_InstanceID;\n//@hook vertex\n }\n#endif\n#ifdef INSTANCED\n vec4 placed = instanceMatrix * vec4(local, 1.0);\n // the instance's normal matrix as its cofactor: the inverse transpose up to scale, from three\n // cross products, flipped back for mirroring transforms\n mat3 m = mat3(instanceMatrix);\n mat3 instanceNormal = mat3(cross(m[1], m[2]), cross(m[2], m[0]), cross(m[0], m[1])) * sign(dot(m[0], cross(m[1], m[2])));\n vec3 placedNormal = instanceNormal * n;\n vColor *= instanceColor;\n if (gl_InstanceID == uHighlightIndex) vHighlight = 1.0;\n#else\n vec4 placed = vec4(local, 1.0);\n vec3 placedNormal = n;\n#endif\n#ifdef VERTEX_COLORS\n vColor *= srgbToLinear(color);\n#endif\n vec4 world = uModel * placed;\n vWorldPosition = world.xyz;\n vNormal = uNormalMatrix * placedNormal;\n vUv = uv;\n#ifdef UV2\n vUv2 = uv2;\n#endif\n#ifdef ANISOTROPY\n // a direction along the surface, so it turns as the model does\n#ifdef INSTANCED\n vTangent = vec4(mat3(uModel) * (m * tangent.xyz), tangent.w);\n#else\n vTangent = vec4(mat3(uModel) * tangent.xyz, tangent.w);\n#endif\n#endif\n#ifdef TRIPLANAR\n vObjectPosition = local;\n vObjectNormal = n;\n#ifdef INSTANCED\n vObjectToWorld = uNormalMatrix * instanceNormal;\n#else\n vObjectToWorld = uNormalMatrix;\n#endif\n#endif\n gl_Position = uViewProjection * world;\n#ifdef FOG\n vFogDepth = -(uView * world).z;\n#endif\n#ifdef PICK\n vPickElement = uint(gl_InstanceID);\n#endif\n}\n";
|
|
30
|
+
export declare const POINTS_VERTEX = "\nlayout(location = 0) in vec3 position;\nlayout(location = 3) in vec3 color;\n#ifdef SPRITES\n// each point a square: one instance a point, drawn as a strip over the corners (along 0-1, across -1-1)\nlayout(location = 4) in vec2 corner;\nlayout(location = 11) in float pointSize;\nlayout(location = 12) in float pointOpacity;\nlayout(location = 13) in float pointAngle;\nlayout(location = 14) in float pointIndex;\nuniform vec2 uResolution; // drawing buffer size in pixels\nout vec2 vUv;\nout float vOpacity;\n#endif\n\nuniform mat4 uModel;\nuniform mat4 uView;\nuniform mat4 uProjection;\nuniform float uSize; // pixels (with the pixel ratio applied), or world units when attenuated\nuniform float uScale; // projection focal term x half the buffer height: world size -> pixels at depth 1\nuniform int uHighlightIndex; // -2 none, -1 every point, otherwise the point\n//@hook uniforms\n\nout vec3 vColor;\nflat out float vHighlight;\n#ifdef HOOK_FRAGMENT\nflat out float vPointIndex;\n#endif\n#ifdef PICK\nflat out uint vPickElement; // which instance, point or segment, for GPU picking\n#endif\n#ifdef FOG\nout float vFogDepth;\n#endif\n\n\nvec3 srgbToLinear(vec3 c) {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n}\nvec3 linearToSrgb(vec3 c) {\n c = clamp(c, 0.0, 1.0);\n return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\n\nvoid main() {\n // the point in the geometry's own space, and its size as a multiple of the material's - where a\n // shader hook changes them\n vec3 local = position;\n#ifdef SPRITES\n int self = int(pointIndex + 0.5);\n float size = pointSize;\n#else\n int self = gl_VertexID;\n float size = 1.0;\n#endif\n#ifdef HOOK_VERTEX\n {\n int index = self;\n//@hook vertex\n }\n#endif\n#ifdef HOOK_FRAGMENT\n vPointIndex = float(self);\n#endif\n vec4 viewPosition = uView * (uModel * vec4(local, 1.0));\n#ifdef SPRITES\n // the square's corner, turned by the point's angle: the texture turns with it\n vec2 c = vec2(corner.x * 2.0 - 1.0, corner.y);\n float s = sin(pointAngle), k = cos(pointAngle);\n vec2 turned = vec2(c.x * k - c.y * s, c.x * s + c.y * k);\n float across = uSize * size;\n#ifdef SIZE_ATTENUATION\n // world units: the square sits in the view, facing the camera\n viewPosition.xy += turned * across * 0.5;\n gl_Position = uProjection * viewPosition;\n#else\n // pixels: the square is laid out on the screen\n gl_Position = uProjection * viewPosition;\n gl_Position.xy += turned * across / uResolution * gl_Position.w;\n#endif\n // the image upright on the square, its top at the top, as a point's is\n vUv = vec2(c.x * 0.5 + 0.5, 0.5 - c.y * 0.5);\n vOpacity = pointOpacity;\n#else\n gl_Position = uProjection * viewPosition;\n#endif\n#ifdef FOG\n vFogDepth = -viewPosition.z;\n#endif\n#ifdef SPRITES\n#elif defined(SIZE_ATTENUATION)\n // perspective shrinks with distance; an orthographic projection (w row 0, 0, 0, 1) doesn't\n float depth = uProjection[3][3] == 1.0 ? 1.0 : max(-viewPosition.z, 1e-4);\n gl_PointSize = uSize * size * uScale / depth;\n#else\n gl_PointSize = uSize * size;\n#endif\n vColor = vec3(1.0);\n#ifdef VERTEX_COLORS\n vColor = srgbToLinear(color);\n#endif\n vHighlight = uHighlightIndex == -1 || self == uHighlightIndex ? 1.0 : 0.0;\n#ifdef PICK\n vPickElement = uint(self);\n#endif\n}\n";
|
|
31
|
+
export declare const LINES_VERTEX = "\nlayout(location = 0) in vec3 segmentStart;\nlayout(location = 1) in vec3 segmentEnd;\nlayout(location = 2) in vec3 segmentColorStart;\nlayout(location = 3) in vec3 segmentColorEnd;\n// the distance along its chain (segments joined end to start) where it starts, in the model's\n// units; 1 if it starts a chain; 1 if it ends one\nlayout(location = 5) in vec3 segmentInfo;\n\nuniform mat4 uModel;\nuniform mat4 uView;\nuniform mat4 uProjection;\nuniform vec2 uResolution; // drawing buffer size in pixels\nuniform float uWidth; // pixels, already scaled by the pixel ratio\nuniform float uNear;\nuniform int uArrows; // arrowheads: 1 at the start of each chain, 2 at its end, 3 at both\nuniform float uArrowSize; // their length in pixels, scaled by the pixel ratio\n//@hook uniforms\n\n// six vertices an instance make the segment's quad, as two triangles - x: 0 at its start, 1 at its\n// end; y: -1 or 1 across - and six more its two arrowheads, drawn only when asked for\nconst vec2 QUAD[6] = vec2[6](vec2(0.0, -1.0), vec2(0.0, 1.0), vec2(1.0, -1.0), vec2(1.0, -1.0), vec2(0.0, 1.0), vec2(1.0, 1.0));\n\nout vec3 vColor;\n#if defined(DASHED) || defined(HOOK_FRAGMENT)\nout float vDistance; // along the chain, in the model's units\n#endif\n#ifdef HOOK_FRAGMENT\nflat out float vSegment; // which segment, for a fragment hook's index\n#endif\n#ifdef DASHED\nflat out float vArrow; // 1 on an arrowhead, which is never dashed\n#endif\n#ifdef PICK\nflat out uint vPickElement; // which instance, point or segment, for GPU picking\n#endif\n#ifdef FOG\nout float vFogDepth;\n#endif\n\n\nvec3 srgbToLinear(vec3 c) {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n}\nvec3 linearToSrgb(vec3 c) {\n c = clamp(c, 0.0, 1.0);\n return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\n\n#ifdef HOOK_VERTEX\n// a shader hook moves each end of each segment: local is the end, along 0 at the start and 1 at the end\nvec3 hookVertex(vec3 local, float along, int index) {\n//@hook vertex\n return local;\n}\n#endif\n\nvoid main() {\n vec3 start = segmentStart, end = segmentEnd;\n#ifdef HOOK_VERTEX\n start = hookVertex(segmentStart, 0.0, gl_InstanceID);\n end = hookVertex(segmentEnd, 1.0, gl_InstanceID);\n#endif\n vec4 a = uView * (uModel * vec4(start, 1.0));\n vec4 b = uView * (uModel * vec4(end, 1.0));\n // clip the segment to the near plane so lines passing behind the camera stay correct\n float clipZ = -uNear;\n if (a.z > clipZ && b.z > clipZ) {\n gl_Position = vec4(2.0, 2.0, 2.0, 1.0); // wholly behind: off screen\n return;\n }\n if (a.z > clipZ) a = mix(a, b, (clipZ - a.z) / (b.z - a.z));\n if (b.z > clipZ) b = mix(b, a, (clipZ - b.z) / (a.z - b.z));\n\n vec4 ca = uProjection * a;\n vec4 cb = uProjection * b;\n vec2 sa = ca.xy / ca.w * uResolution * 0.5;\n vec2 sb = cb.xy / cb.w * uResolution * 0.5;\n vec2 dir = sb - sa;\n float len = length(dir);\n dir = len > 0.0 ? dir / len : vec2(1.0, 0.0);\n vec2 normal = vec2(-dir.y, dir.x);\n\n bool arrowStart = (uArrows & 1) != 0 && segmentInfo.y > 0.5;\n bool arrowEnd = (uArrows & 2) != 0 && segmentInfo.z > 0.5;\n float along, arrow = 0.0;\n vec2 offset;\n if (gl_VertexID < 6) {\n vec2 c = QUAD[gl_VertexID];\n along = c.x;\n // past each end by half the width so joins look continuous - or short of it, under an arrowhead\n float startOut = arrowStart ? -uArrowSize * 0.8 : uWidth * 0.5;\n float endOut = arrowEnd ? -uArrowSize * 0.8 : uWidth * 0.5;\n offset = normal * c.y * uWidth * 0.5 + dir * (along > 0.5 ? endOut : -startOut);\n } else {\n // an arrowhead: its tip on the chain's end, pointing out along the segment\n int which = (gl_VertexID - 6) / 3, k = gl_VertexID - 6 - which * 3;\n if (which == 0 ? !arrowStart : !arrowEnd) {\n gl_Position = vec4(2.0, 2.0, 2.0, 1.0);\n return;\n }\n along = float(which);\n vec2 d = which == 1 ? dir : -dir;\n float wing = uArrowSize * 0.45;\n offset = k == 0 ? vec2(0.0) : -d * uArrowSize + normal * (k == 1 ? wing : -wing);\n arrow = 1.0;\n }\n\n vec4 clip = mix(ca, cb, along);\n#ifdef FOG\n vFogDepth = -mix(a.z, b.z, along);\n#endif\n clip.xy += offset / (uResolution * 0.5) * clip.w;\n gl_Position = clip;\n#if defined(DASHED) || defined(HOOK_FRAGMENT)\n vDistance = segmentInfo.x + length(end - start) * along;\n#endif\n#ifdef HOOK_FRAGMENT\n vSegment = float(gl_InstanceID);\n#endif\n#ifdef DASHED\n vArrow = arrow;\n#endif\n#ifdef PICK\n vPickElement = uint(gl_InstanceID);\n#endif\n\n vColor = vec3(1.0);\n#ifdef VERTEX_COLORS\n vColor = srgbToLinear(mix(segmentColorStart, segmentColorEnd, along));\n#endif\n}\n";
|
|
32
32
|
export type ShaderKind = 'mesh' | 'points' | 'lines';
|
|
33
|
+
export type HookSource = {
|
|
34
|
+
/**
|
|
35
|
+
* declarations
|
|
36
|
+
*/
|
|
37
|
+
uniforms: string;
|
|
38
|
+
/**
|
|
39
|
+
* functions every hook can call - hash, noise, fbm
|
|
40
|
+
*/
|
|
41
|
+
helpers?: string;
|
|
42
|
+
vertex: string;
|
|
43
|
+
fragment: string;
|
|
44
|
+
lit: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* GLSL of a material's own, spliced in at the shaders' `//@hook` lines: uniform declarations, and
|
|
48
|
+
* code run on each vertex, before lighting and after it.
|
|
49
|
+
* @typedef {object} HookSource
|
|
50
|
+
* @property {string} uniforms declarations
|
|
51
|
+
* @property {string} [helpers] functions every hook can call - hash, noise, fbm
|
|
52
|
+
* @property {string} vertex
|
|
53
|
+
* @property {string} fragment
|
|
54
|
+
* @property {string} lit
|
|
55
|
+
*/
|
|
56
|
+
export declare const HOOK_HELPERS = "\nhighp uint phoriaMix(highp uint h) {\n h ^= h >> 16u; h *= 0x7feb352du; h ^= h >> 15u; h *= 0x846ca68bu; h ^= h >> 16u;\n return h;\n}\nfloat phoriaUnit(highp uint h) { return float(h >> 8u) * (1.0 / 16777216.0); }\nfloat hash(float x) { return phoriaUnit(phoriaMix(floatBitsToUint(x))); }\nfloat hash(vec2 p) { highp uvec2 u = floatBitsToUint(p); return phoriaUnit(phoriaMix(u.x ^ phoriaMix(u.y + 0x9e3779b9u))); }\nfloat hash(vec3 p) { highp uvec3 u = floatBitsToUint(p); return phoriaUnit(phoriaMix(u.x ^ phoriaMix(u.y ^ phoriaMix(u.z + 0x9e3779b9u)))); }\n// a lattice point's bits: each coordinate times its own large odd number, mixed once\nhighp uint phoriaCell(highp ivec3 c) {\n highp uvec3 u = uvec3(c) * uvec3(0x8da6b343u, 0xd8163841u, 0xcb1ab31fu);\n return phoriaMix(u.x ^ u.y ^ u.z);\n}\n\nfloat vnoise(vec3 p) {\n vec3 i = floor(p), f = p - i;\n vec3 s = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n highp ivec3 c = ivec3(i);\n float a = mix(phoriaUnit(phoriaCell(c)), phoriaUnit(phoriaCell(c + ivec3(1, 0, 0))), s.x);\n float b = mix(phoriaUnit(phoriaCell(c + ivec3(0, 1, 0))), phoriaUnit(phoriaCell(c + ivec3(1, 1, 0))), s.x);\n float d = mix(phoriaUnit(phoriaCell(c + ivec3(0, 0, 1))), phoriaUnit(phoriaCell(c + ivec3(1, 0, 1))), s.x);\n float e = mix(phoriaUnit(phoriaCell(c + ivec3(0, 1, 1))), phoriaUnit(phoriaCell(c + ivec3(1, 1, 1))), s.x);\n return mix(mix(a, b, s.y), mix(d, e, s.y), s.z);\n}\nfloat vnoise(vec2 p) {\n vec2 i = floor(p), f = p - i;\n vec2 s = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n highp ivec3 c = ivec3(ivec2(i), 0);\n return mix(mix(phoriaUnit(phoriaCell(c)), phoriaUnit(phoriaCell(c + ivec3(1, 0, 0))), s.x),\n mix(phoriaUnit(phoriaCell(c + ivec3(0, 1, 0))), phoriaUnit(phoriaCell(c + ivec3(1, 1, 0))), s.x), s.y);\n}\n\n// simplex: a corner's share of the value and the gradient - t^4 times the gradient's ramp, falling\n// to nothing at the corner's reach\nvec2 phoriaGrad2(highp ivec2 c) {\n float a = phoriaUnit(phoriaCell(ivec3(c, 7))) * 6.28318530718;\n return vec2(cos(a), sin(a));\n}\nvoid phoriaCorner2(vec2 x, highp ivec2 c, inout float n, inout vec2 d) {\n float t = 0.5 - dot(x, x);\n if (t <= 0.0) return;\n vec2 g = phoriaGrad2(c);\n float t2 = t * t, gx = dot(g, x);\n n += t2 * t2 * gx;\n d += t2 * t2 * g - 8.0 * t2 * t * gx * x;\n}\nvec3 snoised(vec2 p) {\n const float F = 0.366025403784, G = 0.211324865405;\n vec2 i = floor(p + (p.x + p.y) * F);\n vec2 x0 = p - i + (i.x + i.y) * G;\n vec2 o = x0.x > x0.y ? vec2(1.0, 0.0) : vec2(0.0, 1.0);\n highp ivec2 c = ivec2(i);\n float n = 0.0;\n vec2 d = vec2(0.0);\n phoriaCorner2(x0, c, n, d);\n phoriaCorner2(x0 - o + G, c + ivec2(o), n, d);\n phoriaCorner2(x0 - 1.0 + 2.0 * G, c + 1, n, d);\n // unit gradients peak near 1/70: scaled to -1-1, then to 0-1\n return vec3(0.5 + 35.0 * n, 35.0 * d);\n}\nfloat snoise(vec2 p) { return snoised(p).x; }\n\nconst vec3 PHORIA_EDGES[12] = vec3[12](vec3(1, 1, 0), vec3(-1, 1, 0), vec3(1, -1, 0), vec3(-1, -1, 0),\n vec3(1, 0, 1), vec3(-1, 0, 1), vec3(1, 0, -1), vec3(-1, 0, -1), vec3(0, 1, 1), vec3(0, -1, 1), vec3(0, 1, -1), vec3(0, -1, -1));\nvoid phoriaCorner3(vec3 x, highp ivec3 c, inout float n, inout vec3 d) {\n float t = 0.6 - dot(x, x);\n if (t <= 0.0) return;\n vec3 g = PHORIA_EDGES[phoriaCell(c) % 12u];\n float t2 = t * t, gx = dot(g, x);\n n += t2 * t2 * gx;\n d += t2 * t2 * g - 8.0 * t2 * t * gx * x;\n}\nvec4 snoised(vec3 p) {\n const float F = 1.0 / 3.0, G = 1.0 / 6.0;\n vec3 i = floor(p + dot(p, vec3(F)));\n vec3 x0 = p - i + dot(i, vec3(G));\n vec3 g = step(x0.yzx, x0.xyz), l = 1.0 - g;\n vec3 i1 = min(g, l.zxy), i2 = max(g, l.zxy);\n highp ivec3 c = ivec3(i);\n float n = 0.0;\n vec3 d = vec3(0.0);\n phoriaCorner3(x0, c, n, d);\n phoriaCorner3(x0 - i1 + G, c + ivec3(i1), n, d);\n phoriaCorner3(x0 - i2 + 2.0 * G, c + ivec3(i2), n, d);\n phoriaCorner3(x0 - 1.0 + 3.0 * G, c + 1, n, d);\n // edge gradients peak near 1/32: scaled to -1-1, then to 0-1\n return vec4(0.5 + 16.0 * n, 16.0 * d);\n}\nfloat snoise(vec3 p) { return snoised(p).x; }\n\n// fbm: each octave turned (so grids don't line up into a visible lattice), scaled by 2.03 and moved\n// on, its amplitude halved; the sum divided by the amplitudes, so 0-1 like the noise\nconst mat2 PHORIA_TURN2 = mat2(0.8, 0.6, -0.6, 0.8);\nconst mat3 PHORIA_TURN3 = mat3(0.00, 0.80, 0.60, -0.80, 0.36, -0.48, -0.60, -0.48, 0.64);\nfloat vfbm(vec2 p, int octaves) {\n float v = 0.0, a = 0.5, sum = 0.0;\n for (int i = 0; i < clamp(octaves, 1, 12); i++) { v += a * vnoise(p); sum += a; p = PHORIA_TURN2 * p * 2.03 + 17.0; a *= 0.5; }\n return v / sum;\n}\nfloat vfbm(vec3 p, int octaves) {\n float v = 0.0, a = 0.5, sum = 0.0;\n for (int i = 0; i < clamp(octaves, 1, 12); i++) { v += a * vnoise(p); sum += a; p = PHORIA_TURN3 * p * 2.03 + 17.0; a *= 0.5; }\n return v / sum;\n}\nfloat sfbm(vec2 p, int octaves) {\n float v = 0.0, a = 0.5, sum = 0.0;\n for (int i = 0; i < clamp(octaves, 1, 12); i++) { v += a * snoise(p); sum += a; p = PHORIA_TURN2 * p * 2.03 + 17.0; a *= 0.5; }\n return v / sum;\n}\nfloat sfbm(vec3 p, int octaves) {\n float v = 0.0, a = 0.5, sum = 0.0;\n for (int i = 0; i < clamp(octaves, 1, 12); i++) { v += a * snoise(p); sum += a; p = PHORIA_TURN3 * p * 2.03 + 17.0; a *= 0.5; }\n return v / sum;\n}\n// with the gradient: each octave's, carried back through the turns and scalings so far\nvec3 sfbmd(vec2 p, int octaves) {\n float v = 0.0, a = 0.5, sum = 0.0;\n vec2 d = vec2(0.0);\n mat2 m = mat2(1.0);\n for (int i = 0; i < clamp(octaves, 1, 12); i++) {\n vec3 n = snoised(p);\n v += a * n.x;\n d += a * (transpose(m) * n.yz);\n sum += a;\n p = PHORIA_TURN2 * p * 2.03 + 17.0;\n m = PHORIA_TURN2 * m * 2.03;\n a *= 0.5;\n }\n return vec3(v, d) / sum;\n}\nvec4 sfbmd(vec3 p, int octaves) {\n float v = 0.0, a = 0.5, sum = 0.0;\n vec3 d = vec3(0.0);\n mat3 m = mat3(1.0);\n for (int i = 0; i < clamp(octaves, 1, 12); i++) {\n vec4 n = snoised(p);\n v += a * n.x;\n d += a * (transpose(m) * n.yzw);\n sum += a;\n p = PHORIA_TURN3 * p * 2.03 + 17.0;\n m = PHORIA_TURN3 * m * 2.03;\n a *= 0.5;\n }\n return vec4(v, d) / sum;\n}\nfloat vfbm(vec2 p) { return vfbm(p, 5); }\nfloat vfbm(vec3 p) { return vfbm(p, 5); }\nfloat sfbm(vec2 p) { return sfbm(p, 5); }\nfloat sfbm(vec3 p) { return sfbm(p, 5); }\nvec3 sfbmd(vec2 p) { return sfbmd(p, 5); }\nvec4 sfbmd(vec3 p) { return sfbmd(p, 5); }\n\n// cellular noise: each unit cell's own point, from its hash; the nearest and second nearest of the\n// 3 x 3 (or 3 x 3 x 3) cells round p - F1, F2 - and the nearest cell's own random number\nvec3 voronoi(vec2 p) {\n vec2 i = floor(p), f = p - i;\n highp ivec2 c = ivec2(i);\n float f1 = 8.0, f2 = 8.0, id = 0.0;\n for (int y = -1; y <= 1; y++) {\n for (int x = -1; x <= 1; x++) {\n highp uint h = phoriaCell(ivec3(c + ivec2(x, y), 11));\n vec2 o = vec2(float(x), float(y)) + vec2(phoriaUnit(h), phoriaUnit(phoriaMix(h ^ 0x68e31da4u))) - f;\n float d = dot(o, o);\n if (d < f1) { f2 = f1; f1 = d; id = phoriaUnit(phoriaMix(h + 0x1b56c4e9u)); }\n else if (d < f2) f2 = d;\n }\n }\n return vec3(sqrt(f1), sqrt(f2), id);\n}\nvec3 voronoi(vec3 p) {\n vec3 i = floor(p), f = p - i;\n highp ivec3 c = ivec3(i);\n float f1 = 8.0, f2 = 8.0, id = 0.0;\n for (int z = -1; z <= 1; z++) {\n for (int y = -1; y <= 1; y++) {\n for (int x = -1; x <= 1; x++) {\n highp uint h = phoriaCell(c + ivec3(x, y, z)) ^ 0x2545f491u;\n h = phoriaMix(h);\n vec3 o = vec3(float(x), float(y), float(z)) - f\n + vec3(phoriaUnit(h), phoriaUnit(phoriaMix(h ^ 0x68e31da4u)), phoriaUnit(phoriaMix(h ^ 0xb5297a4du)));\n float d = dot(o, o);\n if (d < f1) { f2 = f1; f1 = d; id = phoriaUnit(phoriaMix(h + 0x1b56c4e9u)); }\n else if (d < f2) f2 = d;\n }\n }\n }\n return vec3(sqrt(f1), sqrt(f2), id);\n}\n\n// a colour as picked (sRGB) to the linear colour hooks work in\nvec3 srgb(vec3 c) { return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(vec3(0.04045), c)); }\n// hue, saturation and value 0-1 to a linear colour - the colour a colour picker shows\nvec3 hsv2rgb(vec3 hsv) {\n vec3 k = clamp(abs(mod(hsv.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);\n return srgb(hsv.z * mix(vec3(1.0), k, hsv.y));\n}\nvec3 hsv2rgb(float h, float s, float v) { return hsv2rgb(vec3(h, s, v)); }\n// a cosine palette (Quilez): a + b cos(2 pi (c t + d)), its four vectors as picked; a linear colour\nvec3 palette(float t, vec3 a, vec3 b, vec3 c, vec3 d) { return srgb(clamp(a + b * cos(6.28318530718 * (c * t + d)), 0.0, 1.0)); }\n// how edge-on a surface is seen: 0 facing the camera, 1 side on, sharpened by the power - rim light\nfloat fresnel(vec3 normal, vec3 position, float power) {\n vec3 v = normalize(cameraPosition - position);\n return pow(1.0 - clamp(abs(dot(normalize(normal), v)), 0.0, 1.0), power);\n}\n// turning: rotate2(a) * v anticlockwise by a radians; rotate3(axis, a) * v about an axis\nmat2 rotate2(float a) { float s = sin(a), c = cos(a); return mat2(c, s, -s, c); }\nmat3 rotate3(vec3 axis, float a) {\n vec3 u = normalize(axis);\n float s = sin(a), c = cos(a), t = 1.0 - c;\n return mat3(t * u.x * u.x + c, t * u.x * u.y + s * u.z, t * u.x * u.z - s * u.y,\n t * u.x * u.y - s * u.z, t * u.y * u.y + c, t * u.y * u.z + s * u.x,\n t * u.x * u.z + s * u.y, t * u.y * u.z - s * u.x, t * u.z * u.z + c);\n}\n// x from one range to another, not clamped\nfloat remap(float x, float a, float b, float c, float d) { return c + (x - a) * (d - c) / (b - a); }\nvec2 remap(vec2 x, float a, float b, float c, float d) { return c + (x - a) * (d - c) / (b - a); }\nvec3 remap(vec3 x, float a, float b, float c, float d) { return c + (x - a) * (d - c) / (b - a); }\n// clamped to 0-1, as HLSL has it\nfloat saturate(float x) { return clamp(x, 0.0, 1.0); }\nvec2 saturate(vec2 x) { return clamp(x, 0.0, 1.0); }\nvec3 saturate(vec3 x) { return clamp(x, 0.0, 1.0); }\nvec4 saturate(vec4 x) { return clamp(x, 0.0, 1.0); }\n";
|
|
57
|
+
export declare const HOOK_SOURCE_NUMBERS: {
|
|
58
|
+
vertex: number;
|
|
59
|
+
fragment: number;
|
|
60
|
+
lit: number;
|
|
61
|
+
};
|
|
33
62
|
/**
|
|
34
|
-
* Vertex and fragment source for a kind of drawable, or a pass's own pair, with a set of feature
|
|
63
|
+
* Vertex and fragment source for a kind of drawable, or a pass's own pair, with a set of feature
|
|
64
|
+
* defines - and a material's shader hooks, when it has them.
|
|
35
65
|
* @param {ShaderKind | [string, string]} kind
|
|
36
66
|
* @param {string[]} defines e.g. ['LIT', 'MAP']
|
|
67
|
+
* @param {HookSource | null} [hooks]
|
|
37
68
|
* @returns {[string, string]}
|
|
38
69
|
*/
|
|
39
|
-
export declare function shaderSource(kind: ShaderKind | [string, string], defines: string[]): [string, string];
|
|
70
|
+
export declare function shaderSource(kind: ShaderKind | [string, string], defines: string[], hooks?: HookSource | null): [string, string];
|
package/llms-full.txt
CHANGED
|
@@ -147,6 +147,29 @@ Methods:
|
|
|
147
147
|
- `await view.toImage()` - a PNG `Blob` of the picture, labels included.
|
|
148
148
|
- `await view.explain()` - a plain-text report of what is drawn, with warnings ("nothing in view",
|
|
149
149
|
"no lights", labels hidden...). Use it to debug a blank or wrong picture.
|
|
150
|
+
- `view.debug` (or the `debug` option) - pictures for a person checking a scene, drawn over it:
|
|
151
|
+
`true` for all but normals, or `{ lights, shadows, bounds, normals, axes, camera, stats }`.
|
|
152
|
+
- `lights`: an arrow for each directional light (over the scene's middle, the way it shines), a
|
|
153
|
+
small sphere and a ring at its range for point lights, a cone for spot lights - in the light's colour.
|
|
154
|
+
- `shadows`: the box (directional) or cone (spot) each shadow map is drawn from, in yellow - the
|
|
155
|
+
usual reason a shadow is cut off, blocky or soft.
|
|
156
|
+
- `bounds`: each drawable's world box, in cyan - what culling, `view.frame()` and shadow fitting
|
|
157
|
+
see. `'hovered'`: only what is under the pointer (an instance's own box).
|
|
158
|
+
- `normals`: a short magenta line along each vertex normal - for lighting that looks wrong on
|
|
159
|
+
imported or generated geometry. Meshes up to 5,000 vertices when all are shown; `'hovered'` shows
|
|
160
|
+
the mesh under the pointer, whatever its size.
|
|
161
|
+
- `axes`: x, y and z at the origin, red, green and blue. `camera`: the point the orbit turns round.
|
|
162
|
+
- `stats`: a corner readout - fps and frame time, the CPU time of a frame, draw calls, triangles,
|
|
163
|
+
programs, textures and geometries; `true` is top left, or `'top-right'`, `'bottom-left'`,
|
|
164
|
+
`'bottom-right'`.
|
|
165
|
+
|
|
166
|
+
The helpers are lines in the scene's depth that also show faintly through what is in front of
|
|
167
|
+
them. They aren't part of the scene: never picked, framed, saved or counted, and `explain()` says
|
|
168
|
+
when debug drawing is on. Their code loads the first time debug is turned on.
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
view.debug = { lights: true, shadows: true, bounds: 'hovered', stats: 'bottom-right' }
|
|
172
|
+
```
|
|
150
173
|
- `render()`, `invalidate()` (redraw after changing data the view can't see), `dispose()`.
|
|
151
174
|
|
|
152
175
|
If a page shows nothing or the wrong thing, `console.log(await view.explain())` before anything else.
|
|
@@ -468,6 +491,210 @@ on top, as with UVs (animate `offset` to scroll a pattern). 0 (the default) uses
|
|
|
468
491
|
{ bumpMap: pores, bumpScale: 0.0005, roughnessMap: sheen, triplanar: 45 } // bone texture on a scanned skull
|
|
469
492
|
```
|
|
470
493
|
|
|
494
|
+
**Shader hooks** - a few lines of GLSL of your own, run at fixed points inside phoria2's shader, for
|
|
495
|
+
what the material options can't say: waves, wind, dissolves, scanlines, glows, colour by height. You
|
|
496
|
+
write statements, not a shader: lighting, shadows, fog, clipping, picking and outlines keep working -
|
|
497
|
+
a waving flag casts a waving shadow and is picked where it is drawn. Meshes, instanced meshes, points
|
|
498
|
+
and lines all take them, as `material.shader`:
|
|
499
|
+
|
|
500
|
+
```js
|
|
501
|
+
new Mesh({ geometry: plane({ width: 3, depth: 2, segments: 40 }), material: {
|
|
502
|
+
color: '#3a7bd5', doubleSided: true,
|
|
503
|
+
shader: {
|
|
504
|
+
uniforms: { uAmp: 0.25 },
|
|
505
|
+
vertex: 'local.y += sin(local.x * 3.0 + uTime * 3.0) * uAmp;', // a wave, in the geometry's own space
|
|
506
|
+
fragment: 'base.rgb *= 0.8 + 0.2 * sin(vWorldPosition.y * 20.0);', // stripes, before lighting
|
|
507
|
+
lit: 'color += vec3(0.0, 0.1, 0.2) * (0.5 + 0.5 * sin(uTime));', // a glow the light doesn't touch
|
|
508
|
+
},
|
|
509
|
+
} })
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Three hooks, each optional. What each can use (✎ = change it; everything else is read only):
|
|
513
|
+
|
|
514
|
+
| | Mesh / InstancedMesh | Points | Lines |
|
|
515
|
+
|---|---|---|---|
|
|
516
|
+
| `vertex` - each vertex, before it is placed | ✎ `local` (vec3, own space), ✎ `n` (vec3 normal), `uv` (vec2), `index` (int: the instance) | ✎ `local`, ✎ `size` (float, times the material's), `index` (int: the point) | ✎ `local`, `along` (float: 0 at a segment's start, 1 at its end), `index` (int: the segment) - runs once per end, so a segment moves but stays straight |
|
|
517
|
+
| `fragment` - each pixel, before lighting | ✎ `base` (vec4: linear colour, alpha), ✎ `emissive` (vec3), ✎ `normal` (vec3: the shading normal, world space, facing the viewer - bend it for relief lit per pixel), `vUv`, `vWorldPosition`, `vNormal`, `vColor`; `discard` | ✎ `base`, `pointUv` (vec2, 0-1 across the point), `index` (int); `discard` | ✎ `base`, `distance` (float: along the line, in the geometry's units), `index` (int: the segment); `discard` |
|
|
518
|
+
| `lit` - each pixel, after lighting | ✎ `color` (vec3, linear, lit), ✎ `base.a` | ✎ `color`, ✎ `base.a` | ✎ `color`, ✎ `base.a` |
|
|
519
|
+
|
|
520
|
+
Every hook can also read `uTime` - the View's clock in seconds - and the uniforms you give, and
|
|
521
|
+
call functions for randomness and texture without a texture (hooks can't define their own). `p`
|
|
522
|
+
is a vec2 or vec3, and every result is 0 to 1:
|
|
523
|
+
|
|
524
|
+
- `hash(x)` - from a float, vec2 or vec3's exact bits. The same on every GPU for the same bits, so hash
|
|
525
|
+
exact things - `hash(float(index))` gives each point, instance or segment its own random number,
|
|
526
|
+
`hash(floor(vUv * 20.0))` each cell of a grid. An interpolated value such as `vWorldPosition` can
|
|
527
|
+
differ in its last bits from GPU to GPU, and so can its hash.
|
|
528
|
+
- `vnoise(p)` - value noise: random values at whole-number points, eased between. Cheapest; looks
|
|
529
|
+
blockier, its features lined up with the axes.
|
|
530
|
+
- `snoise(p)` - simplex noise: smoother, rounder, no grid to see - the better choice for anything
|
|
531
|
+
natural. About one feature per unit of `p`: scale `p` up for finer detail, `snoise(vUv * 10.0)`,
|
|
532
|
+
`snoise(vec3(local.xz, uTime))` to change over time.
|
|
533
|
+
- `snoised(p)` - simplex noise with its gradient: a vec3 (value, d/dx, d/dy) from a vec2, a vec4
|
|
534
|
+
from a vec3. The slope for a normal, with no extra samples.
|
|
535
|
+
- `vfbm(p)`, `sfbm(p)` - fractal noise: octaves of value or simplex noise added up, each finer and
|
|
536
|
+
half as strong, each turned so their grids don't line up. Five octaves; `sfbm(p, 8)` for more
|
|
537
|
+
detail, fewer for softer and cheaper (1-12). Mostly 0.25-0.75 - the octaves average out.
|
|
538
|
+
- `sfbmd(p)`, `sfbmd(p, octaves)` - simplex fbm with its gradient, as `snoised`: terrain normals.
|
|
539
|
+
- `voronoi(p)` - cellular (Worley) noise: a random point in each unit cell of `p`. Returns a vec3 -
|
|
540
|
+
`x` the distance to the nearest point (F1), `y` to the second nearest (F2), both in cells, and `z` a
|
|
541
|
+
random 0-1 for the nearest point's cell. `y - x` is 0 on the edges between cells: cracks, cells,
|
|
542
|
+
scales, stone, crystals; `z` gives each cell its own colour or height. A vec3 `p` with time as one
|
|
543
|
+
axis makes the cells drift and re-form. Costs 9 cells (vec2) or 27 (vec3) - more than `snoise`.
|
|
544
|
+
|
|
545
|
+
And the everyday ones, with `cameraPosition` (vec3, the camera in world space) to read too:
|
|
546
|
+
|
|
547
|
+
- `srgb(c)` - a colour as picked (sRGB, as in a colour picker or CSS) to the linear colour hooks work
|
|
548
|
+
in: `base.rgb = srgb(vec3(0.75, 0.12, 0.16))` is the red you picked, not a pale pink.
|
|
549
|
+
- `hsv2rgb(h, s, v)` (or a vec3) - hue, saturation and value 0-1 to a linear colour. Hue 0 red, 1/3
|
|
550
|
+
green, 2/3 blue, wrapping: `hsv2rgb(fract(vWorldPosition.y * 0.2 - uTime * 0.1), 0.8, 1.0)`.
|
|
551
|
+
- `palette(t, a, b, c, d)` - Inigo Quilez's cosine palette, `a + b cos(2pi(c t + d))`, its four
|
|
552
|
+
vec3s as picked, the result linear. `palette(t, vec3(0.5), vec3(0.5), vec3(1.0), vec3(0.0, 0.33,
|
|
553
|
+
0.67))` is a rainbow; other a-d give heat, ocean, pastel ramps.
|
|
554
|
+
- `fresnel(normal, position, power)` - how side-on a surface is to the camera, 0 facing it, 1 edge-on,
|
|
555
|
+
sharpened by the power: rim light, holograms, force fields, `fresnel(vNormal, vWorldPosition, 3.0)`.
|
|
556
|
+
- `rotate2(angle)` - a mat2 turning anticlockwise: `rotate2(a) * v`. `rotate3(axis, angle)` - a mat3
|
|
557
|
+
turning about an axis. Twists, spins, turned uvs or noise.
|
|
558
|
+
- `remap(x, inMin, inMax, outMin, outMax)` - from one range to another (not clamped; float, vec2 or
|
|
559
|
+
vec3 x). `saturate(x)` - clamped to 0-1, as HLSL has it.
|
|
560
|
+
|
|
561
|
+
- **`uniforms`**, by GLSL name: a number (float), two to four numbers (vec2-vec4), a colour string or
|
|
562
|
+
Color (vec3, sent linear) or a Texture (sampler2D, read with `texture(uName, uv)`). Change one on
|
|
563
|
+
`material.shader.uniforms` (`mesh.material.shader.uniforms.uAmp = 0.5`) and the View redraws; assign
|
|
564
|
+
a new `material.shader` to change the code. Names starting `gl_` or `phoria`, `uTime`, and phoria2's
|
|
565
|
+
own uniforms (`uColor`, `uModel`, `uOpacity`...) are taken.
|
|
566
|
+
- **`uTime`**: a hook that reads it keeps the View drawing while time runs - no frame loop of your own.
|
|
567
|
+
`view.stop()` freezes it.
|
|
568
|
+
- **Every pass runs the vertex hook** - shadow maps, outlines, GPU picking. A mesh's `fragment`
|
|
569
|
+
`discard` cuts its shadow and its picking too, so a dissolve leaves no shadow of what has gone.
|
|
570
|
+
- **`bounds`**: how far, in world units, the vertex hook can move a vertex. Bounds are padded by it for
|
|
571
|
+
culling, `view.frame()` and shadows; without it a vertex hook turns frustum culling off for the node.
|
|
572
|
+
- **Picking** a node a vertex hook moves is always done on the GPU, so clicks, hovers and tooltips find
|
|
573
|
+
it where it is drawn (the hit has the point, not a normal or uv). `pickAll`, `pickArea` and
|
|
574
|
+
`Raycaster` work from the geometry as given, not as moved.
|
|
575
|
+
- **A hook that doesn't compile** is reported once, by its own line numbers - `shader.vertex line 2:
|
|
576
|
+
'wobble' : undeclared identifier` - with the names it can use, and the material is drawn without its
|
|
577
|
+
hooks. `view.explain()` lists the hooked materials.
|
|
578
|
+
|
|
579
|
+
Recipes - each a material option to copy:
|
|
580
|
+
|
|
581
|
+
```js
|
|
582
|
+
// bumps lit per pixel from any height: bend the shading normal by its slope - crisp at any mesh
|
|
583
|
+
// resolution, no vertices moved (here a surface facing up, its uvs along x and z)
|
|
584
|
+
shader: { fragment: `
|
|
585
|
+
vec2 q = vUv * 20.0;
|
|
586
|
+
float e = 0.01, h = sfbm(q, 4);
|
|
587
|
+
vec2 slope = vec2(sfbm(q + vec2(e, 0.0), 4) - h, sfbm(q + vec2(0.0, e), 4) - h) / e;
|
|
588
|
+
normal = normalize(normal - vec3(slope.x, 0.0, slope.y) * 0.05);` }
|
|
589
|
+
|
|
590
|
+
// a wave with its normal bent to match, so the light follows the folds (a plane, a flag, a sheet)
|
|
591
|
+
shader: { uniforms: { uAmp: 0.2 }, vertex: `
|
|
592
|
+
float phase = local.x * 3.0 - uTime * 4.0;
|
|
593
|
+
local.y += sin(phase) * uAmp;
|
|
594
|
+
n = normalize(vec3(-cos(phase) * 3.0 * uAmp, 1.0, 0.0));` }
|
|
595
|
+
|
|
596
|
+
// ripples spreading from the middle of a plane (give it segments: 64 or so)
|
|
597
|
+
shader: { vertex: `
|
|
598
|
+
float r = length(local.xz), phase = r * 6.0 - uTime * 3.0, h = 0.04 * exp(-r * 0.4);
|
|
599
|
+
local.y += sin(phase) * h;
|
|
600
|
+
vec2 slope = local.xz / max(r, 1e-3) * cos(phase) * 6.0 * h;
|
|
601
|
+
n = normalize(vec3(-slope.x, 1.0, -slope.y));` }
|
|
602
|
+
|
|
603
|
+
// breathing: grow and shrink about the middle
|
|
604
|
+
shader: { vertex: 'local *= 1.0 + 0.06 * sin(uTime * 2.0);' }
|
|
605
|
+
|
|
606
|
+
// dissolve through fractal noise, its edge glowing (its shadow dissolves too)
|
|
607
|
+
shader: { fragment: `
|
|
608
|
+
float level = 0.5 - 0.55 * cos(uTime * 0.6);
|
|
609
|
+
float v = sfbm(vUv * vec2(10.0, 3.0));
|
|
610
|
+
if (v < level) discard;
|
|
611
|
+
emissive += vec3(1.0, 0.2, 0.02) * smoothstep(level + 0.05, level, v) * 5.0;` }
|
|
612
|
+
|
|
613
|
+
// terrain from simplex fbm, its normal from the same call's gradient (a plane with segments: 128
|
|
614
|
+
// or so); the octaves set how rugged - 3 rolling hills, 8 craggy
|
|
615
|
+
shader: { uniforms: { uOctaves: 7 }, vertex: `
|
|
616
|
+
vec3 h = sfbmd(local.xz * 0.4, int(uOctaves));
|
|
617
|
+
local.y += h.x * 2.0;
|
|
618
|
+
n = normalize(vec3(-h.y * 2.0 * 0.4, 1.0, -h.z * 2.0 * 0.4));` }
|
|
619
|
+
|
|
620
|
+
// a texture uniform: scroll a noise or pattern image across a surface
|
|
621
|
+
shader: { uniforms: { uPattern: patternTexture }, fragment: 'base.rgb *= texture(uPattern, vUv * 4.0 + vec2(uTime * 0.1, 0.0)).rgb;' }
|
|
622
|
+
|
|
623
|
+
// colour by height - a ramp from deep blue at y = 0 to orange at y = 2
|
|
624
|
+
shader: { fragment: 'base.rgb = mix(vec3(0.01, 0.05, 0.5), vec3(1.0, 0.25, 0.0), clamp(vWorldPosition.y / 2.0, 0.0, 1.0));' }
|
|
625
|
+
|
|
626
|
+
// rim glow: a hologram or force field - a faint body, bright where it turns away (with
|
|
627
|
+
// transparent: true)
|
|
628
|
+
shader: {
|
|
629
|
+
fragment: 'base.rgb = srgb(vec3(0.25, 0.9, 1.0)) * 0.15; emissive += srgb(vec3(0.25, 0.9, 1.0)) * fresnel(vNormal, vWorldPosition, 2.5) * 2.0;',
|
|
630
|
+
lit: 'base.a *= 0.3 + 0.7 * fresnel(vNormal, vWorldPosition, 1.5);',
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// a twist: each height turned further round the y axis, its normal turned with it (give the mesh segments)
|
|
634
|
+
shader: { vertex: 'mat2 turn = rotate2(local.y * 1.2 * sin(uTime)); local.xz = turn * local.xz; n.xz = turn * n.xz;' }
|
|
635
|
+
|
|
636
|
+
// a hue climbing an object, and a cosine palette across another
|
|
637
|
+
shader: { fragment: 'base.rgb = hsv2rgb(fract(vWorldPosition.y * 0.2 - uTime * 0.1), 0.75, 1.0);' }
|
|
638
|
+
shader: { fragment: 'base.rgb = palette(vUv.x + uTime * 0.05, vec3(0.5), vec3(0.5), vec3(1.0), vec3(0.0, 0.33, 0.67));' }
|
|
639
|
+
|
|
640
|
+
// instances as a meter: each bar's height from one sine, a phase apart by index, remapped and saturated
|
|
641
|
+
shader: { vertex: 'float h = saturate(remap(sin(uTime * 2.0 + float(index) * 0.5), -1.0, 1.0, 0.05, 1.0)); local.y = (local.y + 0.5) * h - 0.5;' }
|
|
642
|
+
|
|
643
|
+
// cracked lava: dark crust plates, each its own shade, molten in the cracks between them (time as
|
|
644
|
+
// the third axis, so the plates drift and re-form)
|
|
645
|
+
shader: { fragment: `
|
|
646
|
+
vec3 v = voronoi(vec3(vUv * 8.0, uTime * 0.2));
|
|
647
|
+
float crack = 1.0 - smoothstep(0.0, 0.12, v.y - v.x);
|
|
648
|
+
base.rgb = srgb(vec3(0.16, 0.12, 0.11)) * (0.5 + 0.9 * v.z);
|
|
649
|
+
emissive += vec3(1.0, 0.25, 0.0) * crack * 4.0;` }
|
|
650
|
+
|
|
651
|
+
// stained glass: each cell a random hue, dark leading between them
|
|
652
|
+
shader: { fragment: 'vec3 v = voronoi(vUv * 6.0); base.rgb = hsv2rgb(v.z, 0.7, 1.0) * smoothstep(0.02, 0.06, v.y - v.x);' }
|
|
653
|
+
|
|
654
|
+
// scanlines over the lit picture
|
|
655
|
+
shader: { lit: 'color *= 0.75 + 0.25 * step(0.5, fract(vWorldPosition.y * 20.0 - uTime));' }
|
|
656
|
+
|
|
657
|
+
// fade with distance from a point (with transparent: true on the material)
|
|
658
|
+
shader: { uniforms: { uCentre: [0, 0, 0] }, lit: 'base.a *= 1.0 - smoothstep(2.0, 4.0, distance(vWorldPosition, uCentre));' }
|
|
659
|
+
|
|
660
|
+
// points: twinkling, each at its own rate, and soft and round
|
|
661
|
+
shader: {
|
|
662
|
+
vertex: 'size *= 1.0 + 0.6 * sin(uTime * (1.5 + hash(float(index)) * 3.0) + float(index));',
|
|
663
|
+
fragment: 'base.a *= smoothstep(1.0, 0.0, length(pointUv * 2.0 - 1.0));',
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// lines: pulses running along them (distance is along each run of joined segments), each line's
|
|
667
|
+
// at its own time
|
|
668
|
+
shader: { fragment: 'base.rgb += vec3(0.3, 0.9, 1.0) * smoothstep(0.85, 1.0, fract(distance * 0.6 - uTime + hash(float(index)))) * 4.0;' }
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
Mistakes to avoid:
|
|
672
|
+
|
|
673
|
+
- **Colours are linear**, as the shader works, so an sRGB-looking value comes out pale. Wrap a colour
|
|
674
|
+
as picked in `srgb()` - `srgb(vec3(0.75, 0.12, 0.16))` - or write linear values, roughly the sRGB
|
|
675
|
+
value squared: orange `vec3(1.0, 0.22, 0.0)`. `hsv2rgb` and `palette` give linear colour already,
|
|
676
|
+
and colour uniforms given as strings are converted for you.
|
|
677
|
+
- **Hooks are statements**, run inside phoria2's `main()`: no functions of your own, no `uniform`
|
|
678
|
+
declarations (use `uniforms`), no `return` in a mesh or points hook. Declare locals as you like.
|
|
679
|
+
`hash`, the noises and the fbms are there for what you'd otherwise write a function for.
|
|
680
|
+
- **Randomness from `hash()`, not `fract(sin(x) * 43758.5453)`**: the sin trick multiplies a GPU's
|
|
681
|
+
last bits of `sin` into the answer, so the same scene comes out differently on different GPUs.
|
|
682
|
+
`hash` is integer arithmetic, exact everywhere.
|
|
683
|
+
- **`index` is an int**: `float(index)` to do sums with it.
|
|
684
|
+
- **Bend the normal when you move vertices**, or the light shades the shape as it was: change `n`
|
|
685
|
+
(meshes) to match, and keep it normalised. Waves on a flat plane need `segments` to bend at all.
|
|
686
|
+
- **Fine relief belongs in the fragment hook**: moving vertices can only be as sharp as the mesh's
|
|
687
|
+
resolution - a steep step (cracks, tiles, grooves) becomes a staircase between them. Bend `normal`
|
|
688
|
+
in the fragment hook instead, lit per pixel, and keep any vertex movement gentle.
|
|
689
|
+
- **Fades need `transparent: true`**: changing `base.a` in an opaque material changes nothing.
|
|
690
|
+
`discard` works either way.
|
|
691
|
+
- **Move in the geometry's own space**: `local` is before the node's position, rotation and scale (and
|
|
692
|
+
an instance's), so a wave along `local.y` turns with the mesh.
|
|
693
|
+
- **Use `vWorldPosition`** for effects fixed in the world - a band at a height, a fade round a point -
|
|
694
|
+
and `vUv` for ones fixed to the surface.
|
|
695
|
+
- **Lines move by their ends**: a straight segment stays straight - give the geometry more segments to
|
|
696
|
+
bend a line.
|
|
697
|
+
|
|
471
698
|
## Lights and cameras
|
|
472
699
|
|
|
473
700
|
| | Options |
|
package/llms.txt
CHANGED
|
@@ -11,7 +11,7 @@ why a page might look wrong. Coordinates are +Y up, in metres; angles in radians
|
|
|
11
11
|
|
|
12
12
|
## Docs
|
|
13
13
|
|
|
14
|
-
- [Full reference](https://kevs3d.co.uk/phoria2/llms-full.txt): the whole API in one page - View, scene graph, drawables, geometry, materials, lights, interaction and dragging, charts, editing, models, motion, particles, plain-data scenes
|
|
14
|
+
- [Full reference](https://kevs3d.co.uk/phoria2/llms-full.txt): the whole API in one page - View, scene graph, drawables, geometry, materials (and shader hooks: GLSL of your own inside the material - waves, dissolves, glows), lights, interaction and dragging, charts, editing, models, motion, particles, plain-data scenes
|
|
15
15
|
- [README](https://github.com/kevinroast/phoria2#readme): install, CDN use, the entry points
|
|
16
16
|
- Source: the package ships its readable, commented source in `node_modules/phoria2/src` - read that, not the minified `dist/`, for anything the reference doesn't settle
|
|
17
17
|
- [Chart spec JSON Schema](https://cdn.jsdelivr.net/npm/phoria2/schema/chart.schema.json): the spec `chart()` takes, as `phoria2/charts/schema.json` in the package
|
package/package.json
CHANGED
package/schema/scene.schema.json
CHANGED
|
@@ -943,6 +943,33 @@
|
|
|
943
943
|
"type": "number",
|
|
944
944
|
"description": "Lines: moves the dashes along the lines - animate it for flow."
|
|
945
945
|
},
|
|
946
|
+
"shader": {
|
|
947
|
+
"type": "object",
|
|
948
|
+
"description": "GLSL hooks run inside phoria2's own shader: vertex (change local, and n on meshes), fragment (change base and emissive before lighting; discard cuts) and lit (change color after lighting). Every hook can read uTime.",
|
|
949
|
+
"additionalProperties": false,
|
|
950
|
+
"properties": {
|
|
951
|
+
"vertex": { "type": "string" },
|
|
952
|
+
"fragment": { "type": "string" },
|
|
953
|
+
"lit": { "type": "string" },
|
|
954
|
+
"uniforms": {
|
|
955
|
+
"type": "object",
|
|
956
|
+
"description": "Values the hooks read, by GLSL name: a number, two to four numbers, a colour string, or a texture description.",
|
|
957
|
+
"additionalProperties": {
|
|
958
|
+
"anyOf": [
|
|
959
|
+
{ "type": "number" },
|
|
960
|
+
{ "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 4 },
|
|
961
|
+
{ "type": "string" },
|
|
962
|
+
{ "type": "object", "required": ["url"] }
|
|
963
|
+
]
|
|
964
|
+
}
|
|
965
|
+
},
|
|
966
|
+
"bounds": {
|
|
967
|
+
"type": "number",
|
|
968
|
+
"minimum": 0,
|
|
969
|
+
"description": "How far, in world units, the vertex hook can move a vertex; without it a vertex hook turns frustum culling off."
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
},
|
|
946
973
|
"alphaTest": {
|
|
947
974
|
"type": "number",
|
|
948
975
|
"description": "Cut-outs: drop pixels with less alpha.",
|
package/src/View.js
CHANGED
|
@@ -29,6 +29,7 @@ import { checkOneOf, checkOptions, checkRange, describe, fail, warnOnce } from '
|
|
|
29
29
|
/** @import { ColorInput, Vec3Input } from './types.js' */
|
|
30
30
|
/** @import { Node } from './scene/Node.js' */
|
|
31
31
|
/** @import { Keyboard, KeyboardOptions } from './keyboard.js' */
|
|
32
|
+
/** @import { DebugDraw, DebugOptions } from './debug.js' */
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* @typedef {'auto' | 'always' | 'manual'} RenderMode
|
|
@@ -76,6 +77,8 @@ import { checkOneOf, checkOptions, checkRange, describe, fail, warnOnce } from '
|
|
|
76
77
|
* text } says what it hears (default: the tooltip's text, the data, or the name). Default false
|
|
77
78
|
* @property {string | null} [description] what the view shows, for screen readers: the canvas's label. With keyboard
|
|
78
79
|
* on and no description, one is made from the scene (charts describe themselves)
|
|
80
|
+
* @property {boolean | DebugOptions} [debug] pictures for checking a scene: lights, shadow cameras, bounds,
|
|
81
|
+
* normals, axes, the orbit target and a stats readout. true shows all but normals. Default false
|
|
79
82
|
*/
|
|
80
83
|
|
|
81
84
|
/** @typedef {'click' | 'hover'} ViewEvent */
|
|
@@ -83,7 +86,12 @@ import { checkOneOf, checkOptions, checkRange, describe, fail, warnOnce } from '
|
|
|
83
86
|
|
|
84
87
|
const OPTIONS = ['canvas', 'scene', 'camera', 'orbit', 'background', 'renderMode', 'framing', 'highlight', 'tooltip', 'pixelRatio',
|
|
85
88
|
'antialias', 'outlineAntialias', 'context', 'toneMapping', 'exposure', 'bloom', 'autoLight', 'maxDelta', 'reducedMotion',
|
|
86
|
-
'keyboard', 'description']
|
|
89
|
+
'keyboard', 'description', 'debug']
|
|
90
|
+
// what debug: true shows - everything but normals, which bristle on a big model
|
|
91
|
+
/** @type {Required<DebugOptions>} */
|
|
92
|
+
const DEBUG_ALL = { lights: true, shadows: true, bounds: true, normals: false, axes: true, camera: true, stats: 'top-left' }
|
|
93
|
+
const DEBUG_OPTIONS = Object.keys(DEBUG_ALL)
|
|
94
|
+
const CORNERS = ['top-left', 'top-right', 'bottom-left', 'bottom-right']
|
|
87
95
|
const FRAMING_OPTIONS = ['keep', 'aspect']
|
|
88
96
|
const HINTS = {
|
|
89
97
|
renderer: 'the View makes its renderer - read it from view.renderer',
|
|
@@ -180,6 +188,12 @@ export class View {
|
|
|
180
188
|
/** @type {Keyboard | null} */ #keyboard = null
|
|
181
189
|
/** @type {Hit | null} */ #keyFocus = null
|
|
182
190
|
/** @type {string | null} */ #description = null
|
|
191
|
+
// debug drawing: its code loads when turned on; the helpers are added to each frame, then taken out
|
|
192
|
+
/** @type {Required<DebugOptions> | null} */ #debug = null
|
|
193
|
+
/** @type {DebugDraw | null} */ #debugDraw = null
|
|
194
|
+
#debugLoading = false
|
|
195
|
+
#drawnAt = 0
|
|
196
|
+
#frameMs = 0
|
|
183
197
|
// the canvas had no role, so describing it gave it one - taken away again with the description
|
|
184
198
|
#gaveRole = false
|
|
185
199
|
// the system's prefers-reduced-motion, kept up to date
|
|
@@ -277,6 +291,7 @@ export class View {
|
|
|
277
291
|
interactive: (label) => labelHandlers(label, 'click').length > 0 || labelHandlers(label, 'hover').length > 0,
|
|
278
292
|
on: (type, label, e) => this.#labelPointer(type, label, e),
|
|
279
293
|
})
|
|
294
|
+
if (options.debug !== undefined) this.debug = options.debug
|
|
280
295
|
|
|
281
296
|
if (options.framing !== undefined) {
|
|
282
297
|
// 'both' is short for { keep: 'both' }
|
|
@@ -366,6 +381,32 @@ export class View {
|
|
|
366
381
|
this.invalidate()
|
|
367
382
|
}
|
|
368
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Debug drawing, or false: what it shows, as DebugOptions. Assign true for everything but normals,
|
|
386
|
+
* options for some, or false. Its code loads the first time it's turned on.
|
|
387
|
+
*
|
|
388
|
+
* view.debug = { lights: true, shadows: true, bounds: 'hovered', stats: 'bottom-right' }
|
|
389
|
+
* @returns {boolean | DebugOptions} the options in force, every one given, or false
|
|
390
|
+
*/
|
|
391
|
+
get debug() { return this.#debug ?? false }
|
|
392
|
+
/** @param {boolean | DebugOptions} v */
|
|
393
|
+
set debug(v) {
|
|
394
|
+
this.#debug = debugOptions(v)
|
|
395
|
+
if (this.#debug && !this.#debugDraw && !this.#debugLoading) {
|
|
396
|
+
this.#debugLoading = true
|
|
397
|
+
import('./debug.js').then(({ DebugDraw }) => {
|
|
398
|
+
this.#debugLoading = false
|
|
399
|
+
if (this.#disposed) return
|
|
400
|
+
this.#debugDraw = new DebugDraw(this.#labels.root)
|
|
401
|
+
this.invalidate()
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
// hovered helpers need the pointer followed
|
|
405
|
+
if (this.#debug && (this.#debug.bounds === 'hovered' || this.#debug.normals === 'hovered')) this.#pickPending = true
|
|
406
|
+
if (!this.#debug) this.#debugDraw?.stats(false, /** @type {any} */ ({}), 0, 0)
|
|
407
|
+
this.invalidate()
|
|
408
|
+
}
|
|
409
|
+
|
|
369
410
|
/**
|
|
370
411
|
* What the view shows, for screen readers - the canvas's label - or null. With keyboard on and no
|
|
371
412
|
* description, one is made from the scene.
|
|
@@ -511,9 +552,28 @@ export class View {
|
|
|
511
552
|
this.scene.updateWorldTree()
|
|
512
553
|
const frame = collect(this.scene, this.camera)
|
|
513
554
|
if (this.autoLight && frame.lights.length === 0) this.#addAutoLights(frame)
|
|
555
|
+
frame.time = this.time
|
|
514
556
|
frame.highlight = this.#highlightFor(this.#hovered)
|
|
515
557
|
this.#occlude(frame)
|
|
558
|
+
// debug helpers go on the end of the frame for drawing, and come off again after: never picked,
|
|
559
|
+
// explained or kept in lastFrame
|
|
560
|
+
const debug = this.#debug && this.#debugDraw
|
|
561
|
+
? this.#debugDraw.add(frame, this.#debug, { camera: this.camera, hovered: this.#hovered, bounds: this.scene.boundsFrom(this.camera) })
|
|
562
|
+
: null
|
|
563
|
+
const started = performance.now()
|
|
516
564
|
this.renderer.renderFrame(frame, { wait })
|
|
565
|
+
if (debug) {
|
|
566
|
+
frame.opaque.length -= debug.opaque
|
|
567
|
+
frame.transparent.length -= debug.transparent
|
|
568
|
+
}
|
|
569
|
+
if (this.#debugDraw) {
|
|
570
|
+
const now = performance.now()
|
|
571
|
+
// time between frames drawn one after another, smoothed; after a pause, none
|
|
572
|
+
const gap = this.#drawnAt ? now - this.#drawnAt : 0
|
|
573
|
+
this.#frameMs = gap > 0 && gap < 250 ? this.#frameMs * 0.9 + gap * 0.1 : 0
|
|
574
|
+
this.#drawnAt = now
|
|
575
|
+
this.#debugDraw.stats(this.#debug?.stats ?? false, this.renderer.info, this.#frameMs, now - started)
|
|
576
|
+
}
|
|
517
577
|
this.#labelStats = this.#labels.update(frame.labels, frame.overlays)
|
|
518
578
|
// a node with its own tooltip needs the tooltip's code
|
|
519
579
|
if (frame.tooltips && !this.#tooltip) this.#loadTooltip()
|
|
@@ -521,7 +581,7 @@ export class View {
|
|
|
521
581
|
this.#dirty = false
|
|
522
582
|
// remember what was drawn - including what drawing changed, such as fitting the camera - so
|
|
523
583
|
// the next frame compares against it and doesn't draw the same picture again
|
|
524
|
-
if (this.renderMode === 'auto') this.#watch.changed(this.scene, this.camera)
|
|
584
|
+
if (this.renderMode === 'auto') this.#watch.changed(this.scene, this.camera, this.time)
|
|
525
585
|
// things may have moved under a still pointer - or the keyboard's focus moved on screen
|
|
526
586
|
if ((this.#pointer || this.#keyFocus) && this.#needsHover()) this.#pickPending = true
|
|
527
587
|
return frame
|
|
@@ -732,7 +792,9 @@ export class View {
|
|
|
732
792
|
const frame = this.lastFrame
|
|
733
793
|
if (!frame) return null
|
|
734
794
|
let work = 0
|
|
735
|
-
for (const { node, geometry, instances } of [...frame.opaque, ...frame.transparent]) {
|
|
795
|
+
for (const { node, geometry, instances, material } of [...frame.opaque, ...frame.transparent]) {
|
|
796
|
+
// a shader hook moves vertices where only the GPU knows: picked there, however small the scene
|
|
797
|
+
if (material.shader?.vertex) return frame
|
|
736
798
|
const mode = /** @type {any} */ (node).drawMode
|
|
737
799
|
work += instances ? instances.count : mode === 'points' ? geometry.drawnVertices : mode === 'lines' ? geometry.drawnCount / 2 : geometry.drawnCount / 3
|
|
738
800
|
}
|
|
@@ -748,7 +810,8 @@ export class View {
|
|
|
748
810
|
#exact(found, frame) {
|
|
749
811
|
const node = /** @type {any} */ (found.item.node)
|
|
750
812
|
const raycaster = new Raycaster().setFromCamera(found.ndc, this.camera)
|
|
751
|
-
|
|
813
|
+
// moved by a shader hook, the CPU's copy isn't where it was drawn: the GPU's depth is the hit
|
|
814
|
+
const hit = node.material.shader?.vertex ? null : raycaster.intersectElement(node, found.element)
|
|
752
815
|
if (hit) return hit
|
|
753
816
|
// the ray grazed an edge the GPU drew: place the hit at the depth the GPU found
|
|
754
817
|
const inverse = new Mat4().copy(frame.camera.viewProjection).invert()
|
|
@@ -977,6 +1040,7 @@ export class View {
|
|
|
977
1040
|
contextLost: !!this.renderer.gl?.isContextLost?.(),
|
|
978
1041
|
interaction: { hover: this.#handlers.hover.size, click: this.#handlers.click.size, tooltip: !!this.#tooltipContent, highlight: !!this.#highlight,
|
|
979
1042
|
keyboard: !!this.#keyboard, reducedMotion: this.reducedMotion, description: this.canvas.getAttribute?.('aria-label') ?? this.#description },
|
|
1043
|
+
debug: this.#debug,
|
|
980
1044
|
})
|
|
981
1045
|
}
|
|
982
1046
|
|
|
@@ -994,6 +1058,7 @@ export class View {
|
|
|
994
1058
|
this.#labels.dispose()
|
|
995
1059
|
this.#tooltip?.dispose()
|
|
996
1060
|
this.#keyboard?.dispose()
|
|
1061
|
+
this.#debugDraw?.dispose()
|
|
997
1062
|
if (this.#description !== null && !this.#keyboard) this.description = null
|
|
998
1063
|
this.renderer.dispose(this.scene)
|
|
999
1064
|
if (this.#ownContext) ownContexts--
|
|
@@ -1041,7 +1106,7 @@ export class View {
|
|
|
1041
1106
|
this.#updateControls(dt)
|
|
1042
1107
|
if (this.#pickPending) this.#updateHover()
|
|
1043
1108
|
const draw = this.#dirty || this.renderMode === 'always' ||
|
|
1044
|
-
(this.renderMode === 'auto' && this.#watch.changed(this.scene, this.camera))
|
|
1109
|
+
(this.renderMode === 'auto' && this.#watch.changed(this.scene, this.camera, this.time))
|
|
1045
1110
|
if (draw) this.render()
|
|
1046
1111
|
} finally {
|
|
1047
1112
|
// the next frame comes even if this one threw - the error still reaches the console, but one
|
|
@@ -1052,7 +1117,12 @@ export class View {
|
|
|
1052
1117
|
|
|
1053
1118
|
#needsHover() {
|
|
1054
1119
|
return this.#handlers.hover.size > 0 || !!this.#tooltipContent || !!this.#highlight || this.#handlers.click.size > 0 ||
|
|
1055
|
-
!!this.lastFrame?.interactive
|
|
1120
|
+
!!this.lastFrame?.interactive || this.#debugHovers()
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/** Does debug draw something for what is under the pointer? */
|
|
1124
|
+
#debugHovers() {
|
|
1125
|
+
return !!this.#debug && (this.#debug.bounds === 'hovered' || this.#debug.normals === 'hovered')
|
|
1056
1126
|
}
|
|
1057
1127
|
|
|
1058
1128
|
// pick under the pointer and tell everyone when the thing under it changes - on the GPU without
|
|
@@ -1097,7 +1167,7 @@ export class View {
|
|
|
1097
1167
|
if (p && hit && this.#tooltip) this.#tooltip.show(hit, p.clientX, p.clientY)
|
|
1098
1168
|
else this.#tooltip?.hide()
|
|
1099
1169
|
if (same) return
|
|
1100
|
-
if (this.#highlightFor(before) || this.#highlightFor(hit)) this.invalidate()
|
|
1170
|
+
if (this.#highlightFor(before) || this.#highlightFor(hit) || this.#debugHovers()) this.invalidate()
|
|
1101
1171
|
if (this.#handlers.click.size || this.lastFrame?.interactive) {
|
|
1102
1172
|
this.canvas.style.cursor = hit && nearestAbove(hit.node, 'onDrag') ? 'grab' : hit && this.#clickable(hit) ? 'pointer' : ''
|
|
1103
1173
|
}
|
|
@@ -1490,3 +1560,31 @@ function resolveCanvas(canvas) {
|
|
|
1490
1560
|
if (!(canvas instanceof HTMLCanvasElement)) fail('View', `'canvas' must be a <canvas> element or a selector, got ${describe(canvas)}`)
|
|
1491
1561
|
return canvas
|
|
1492
1562
|
}
|
|
1563
|
+
|
|
1564
|
+
/**
|
|
1565
|
+
* Debug options checked: true for everything but normals, false or null for none.
|
|
1566
|
+
* @param {boolean | DebugOptions | null} v
|
|
1567
|
+
* @returns {Required<DebugOptions> | null}
|
|
1568
|
+
*/
|
|
1569
|
+
function debugOptions(v) {
|
|
1570
|
+
if (v === false || v === null) return null
|
|
1571
|
+
if (v === true) return { ...DEBUG_ALL }
|
|
1572
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) fail('View', `'debug' is true, false or { ${DEBUG_OPTIONS.join(', ')} }, got ${describe(v)}`)
|
|
1573
|
+
checkOptions('View', v, DEBUG_OPTIONS, { helpers: "list what to draw: { lights: true, bounds: true, ... }", fps: "use 'stats: true'", wireframe: "use new Lines({ geometry: edges(geometry) }) for a wireframe" }, 'debug')
|
|
1574
|
+
/** @type {any} */
|
|
1575
|
+
const o = {}
|
|
1576
|
+
for (const key of DEBUG_OPTIONS) {
|
|
1577
|
+
const value = /** @type {any} */ (v)[key]
|
|
1578
|
+
if (value === undefined) o[key] = false
|
|
1579
|
+
else if (key === 'bounds' || key === 'normals') o[key] = value === 'hovered' ? 'hovered' : checkBool(key, value, "true, false or 'hovered'")
|
|
1580
|
+
else if (key === 'stats') o[key] = value === true ? 'top-left' : value === false ? false : checkOneOf('View', 'debug.stats', value, CORNERS)
|
|
1581
|
+
else o[key] = checkBool(key, value, 'true or false')
|
|
1582
|
+
}
|
|
1583
|
+
return o
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
/** @param {string} key @param {unknown} v @param {string} expected */
|
|
1587
|
+
function checkBool(key, v, expected) {
|
|
1588
|
+
if (typeof v !== 'boolean') fail('View', `debug.${key} is ${expected}, got ${describe(v)}`)
|
|
1589
|
+
return v
|
|
1590
|
+
}
|