@absolutejs/rendering 0.2.0 → 0.4.0

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 CHANGED
@@ -54,3 +54,80 @@ neutral state, restores the caller immediately, and returns the exact supplied
54
54
  Float32Array on completion. It never restores stale renderer state after await.
55
55
  Allocate supported targets and correctly sized buffers before calling these
56
56
  helpers. The helpers do not choose formats, sample counts or capture resolution.
57
+
58
+ ## React Spring with demand-rendered Fiber scenes
59
+
60
+ The optional `@absolutejs/rendering/spring` entry provides `animated.mesh`,
61
+ `animated.group`, `useSpring`, `easings`, and `createFiberSpringHost` for other
62
+ explicit Fiber primitives:
63
+
64
+ ```tsx
65
+ import { animated, useSpring } from '@absolutejs/rendering/spring';
66
+
67
+ function MovingGroup({ x }: { x: number }) {
68
+ const motion = useSpring({ x });
69
+ return <animated.group position-x={motion.x}>{/* scene content */}</animated.group>;
70
+ }
71
+ ```
72
+
73
+ It keeps React Spring's normal scheduler. Three prop updates invalidate the
74
+ owning Fiber root through `applyProps`; DOM-only animation does not render a
75
+ scene and can finish without a mounted Canvas. No `useFrame`, manual animation
76
+ clock, global scheduler replacement, or unconditional render loop is installed.
77
+
78
+ Do not also runtime-import `@react-spring/three`: that entry installs a global
79
+ Fiber-driven scheduler even when its consuming component is never mounted. This
80
+ adapter does not override or repair another module's global scheduler changes.
81
+ Type-only imports from that package are safe. Test your actual application's
82
+ eager import graph as well as isolated components.
83
+
84
+ Use `createFiberSpringHost(['instancedMesh', 'meshStandardMaterial'])` to opt into
85
+ additional hosts without registering every Three constructor. React, Fiber and
86
+ React Spring peers are optional for the core material utilities, but required
87
+ when using this subpath. `@react-spring/three` supplies host types only; it is not
88
+ runtime-imported by this entry. The tested versions are React 19.3, Fiber 9.8.2,
89
+ Three 0.186.1 and React Spring 10.1.2.
90
+
91
+ ## Rigid-object triplanar materials
92
+
93
+ `createObjectTriplanarMaterial` keeps color, normal detail and roughness aligned
94
+ without depending on mesh UVs. Coordinates remain local to the geometry, so a
95
+ moving rigid object does not slide through a world-space texture field.
96
+
97
+ ```ts
98
+ import { createObjectTriplanarMaterial } from '@absolutejs/rendering';
99
+ import { RepeatWrapping, SRGBColorSpace, Vector2 } from 'three';
100
+
101
+ for (const texture of [albedo, normal, roughness]) {
102
+ texture.wrapS = texture.wrapT = RepeatWrapping;
103
+ }
104
+ albedo.colorSpace = SRGBColorSpace; // normal and roughness remain non-color data
105
+ const material = createObjectTriplanarMaterial({
106
+ map: albedo,
107
+ normalMap: normal,
108
+ roughnessMap: roughness,
109
+ normalScale: new Vector2(0.65, 0.65),
110
+ projection: { scale: 1, offset: [0, 0, 0], sharpness: 4 },
111
+ });
112
+ ```
113
+
114
+ Projection scale is repeats per geometry-local unit; UV texture matrices/repeat
115
+ values are replaced by these controls. Offset and scale are copied at creation.
116
+ Sharpness ranges from 1 to 16; scale must be finite, positive and at most 10,000.
117
+ Color uses the texture's color space; roughness uses its green channel. Tangent
118
+ normal maps, including packed RG and negative normal-Y scale, blend as surface
119
+ slopes before perturbing the local normal. Neutral maps preserve base normals.
120
+
121
+ Scope is opaque, rigid geometry. Mesh and non-sheared instanced transforms follow
122
+ Three's normal-matrix conventions. Skinning, active morphs, displacement, bump maps,
123
+ object-space normal maps and video albedo are rejected. Transparent, alpha-test,
124
+ alpha-hash and alpha-to-coverage surfaces are rejected because they need matching
125
+ shadow coverage shaders. Other StandardMaterial channels retain their normal Three
126
+ behavior. The helper is for WebGLRenderer; it is not a WebGPU/TSL implementation.
127
+
128
+ The material borrows textures and does not alter or dispose them. Use
129
+ `cloneRenderMaterial` to retain shader hooks when cloning. Dispose material and
130
+ owned textures separately. Standard lighting, shadows and geometry stay intact.
131
+ With color, normal and roughness present, the shader uses nine texture samples
132
+ instead of three UV samples, with no extra draw or render pass. Hardware cost is
133
+ scene-dependent; the qualification uses software rendering, not a hardware FPS claim.
package/dist/index.d.ts CHANGED
@@ -12,3 +12,5 @@ export declare function bindAlphaToCoverageDestination(material: Material, enabl
12
12
  * cannot alter another. No source material is mutated. */
13
13
  export declare function cloneRenderMaterial<T extends Material>(source: T): T;
14
14
  export { renderTargetScope, readTargetPixelsScoped, readFloatTargetScoped, } from "./renderTargetScope.js";
15
+ export { createObjectTriplanarMaterial } from "./objectTriplanar.js";
16
+ export type { ObjectTriplanarProjection, ObjectTriplanarMaterialParameters, } from "./objectTriplanar.js";
package/dist/index.js CHANGED
@@ -76,3 +76,4 @@ export function cloneRenderMaterial(source) {
76
76
  return copy;
77
77
  }
78
78
  export { renderTargetScope, readTargetPixelsScoped, readFloatTargetScoped, } from "./renderTargetScope.js";
79
+ export { createObjectTriplanarMaterial } from "./objectTriplanar.js";
@@ -0,0 +1,18 @@
1
+ import { MeshStandardMaterial, type MeshStandardMaterialParameters } from "three";
2
+ export type ObjectTriplanarProjection = {
3
+ /** Repeats per geometry-local unit; does not swim when an object moves. */
4
+ scale?: number;
5
+ /** Stable geometry-local phase, shared by color, normal and roughness. */
6
+ offset?: readonly [number, number, number];
7
+ /** Axis blending exponent. One is broad; larger values favor the dominant axis. */
8
+ sharpness?: number;
9
+ };
10
+ export type ObjectTriplanarMaterialParameters = MeshStandardMaterialParameters & {
11
+ projection?: ObjectTriplanarProjection;
12
+ };
13
+ /** UV-independent color, tangent-normal and roughness projection for rigid meshes.
14
+ * Borrows textures. Uses MeshStandardMaterial lighting/shadows and one draw.
15
+ * Instancing follows Three's non-sheared instance normal transform. Skinning,
16
+ * active morphs and displacement are outside this material's rigid-object scope.
17
+ * Use cloneRenderMaterial when cloning so the authored shader hooks are retained. */
18
+ export declare function createObjectTriplanarMaterial(options?: ObjectTriplanarMaterialParameters): MeshStandardMaterial;
@@ -0,0 +1,149 @@
1
+ import { MeshStandardMaterial, TangentSpaceNormalMap, Vector3, } from "three";
2
+ const revision = "absolute-object-triplanar-1";
3
+ function replaceOnce(source, anchor, replacement) {
4
+ if (source.split(anchor).length !== 2)
5
+ throw Error(`Unsupported Three shader anchor: ${anchor}`);
6
+ return source.replace(anchor, replacement);
7
+ }
8
+ const varying = `
9
+ varying vec3 absTriPosition;
10
+ varying vec3 absTriObjectNormal;
11
+ varying mat3 absTriNormalToView;
12
+ `;
13
+ const functions = `
14
+ ${varying}
15
+ uniform float absTriScale;
16
+ uniform vec3 absTriOffset;
17
+ uniform float absTriSharpness;
18
+ vec4 absTriSample(sampler2D tex, vec3 p, vec3 dx, vec3 dy, vec3 w) {
19
+ return textureGrad(tex,p.yz,dx.yz,dy.yz)*w.x
20
+ +textureGrad(tex,p.xz,dx.xz,dy.xz)*w.y
21
+ +textureGrad(tex,p.xy,dx.xy,dy.xy)*w.z;
22
+ }
23
+ #ifdef USE_NORMALMAP_TANGENTSPACE
24
+ vec2 absTriSlope(vec3 encoded) {
25
+ vec3 n=encoded*2.-1.;
26
+ #ifdef USE_PACKED_NORMALMAP
27
+ n.z=sqrt(max(0.,1.-dot(n.xy,n.xy)));
28
+ #endif
29
+ return -n.xy*normalScale/max(n.z,.1);
30
+ }
31
+ vec3 absTriGradient(vec3 p,vec3 dx,vec3 dy,vec3 w) {
32
+ vec2 x=absTriSlope(textureGrad(normalMap,p.yz,dx.yz,dy.yz).xyz);
33
+ vec2 y=absTriSlope(textureGrad(normalMap,p.xz,dx.xz,dy.xz).xyz);
34
+ vec2 z=absTriSlope(textureGrad(normalMap,p.xy,dx.xy,dy.xy).xyz);
35
+ return vec3(0.,x.x,x.y)*w.x+vec3(y.x,0.,y.y)*w.y+vec3(z.x,z.y,0.)*w.z;
36
+ }
37
+ #endif
38
+ `;
39
+ const prepare = `
40
+ vec3 absTriN=normalize(absTriObjectNormal);
41
+ #ifdef FLAT_SHADED
42
+ vec3 absTriArea=cross(dFdx(absTriPosition),dFdy(absTriPosition));
43
+ if(dot(absTriArea,absTriArea)>1e-20) {
44
+ absTriArea=normalize(absTriArea);
45
+ absTriN=dot(absTriArea,absTriN)<0.?-absTriArea:absTriArea;
46
+ }
47
+ #endif
48
+ vec3 absTriW=pow(abs(absTriN),vec3(absTriSharpness));
49
+ absTriW/=max(dot(absTriW,vec3(1.)),1e-20);
50
+ vec3 absTriP=absTriPosition*absTriScale+absTriOffset;
51
+ vec3 absTriDx=dFdx(absTriP),absTriDy=dFdy(absTriP);
52
+ `;
53
+ /** UV-independent color, tangent-normal and roughness projection for rigid meshes.
54
+ * Borrows textures. Uses MeshStandardMaterial lighting/shadows and one draw.
55
+ * Instancing follows Three's non-sheared instance normal transform. Skinning,
56
+ * active morphs and displacement are outside this material's rigid-object scope.
57
+ * Use cloneRenderMaterial when cloning so the authored shader hooks are retained. */
58
+ export function createObjectTriplanarMaterial(options = {}) {
59
+ const { projection = {}, ...parameters } = options;
60
+ const scale = projection.scale ?? 1;
61
+ const sharpness = projection.sharpness ?? 4;
62
+ const offset = [...(projection.offset ?? [0, 0, 0])];
63
+ if (!Number.isFinite(scale) || scale <= 0 || scale > 10000)
64
+ throw Error("Invalid triplanar scale");
65
+ if (!Number.isFinite(sharpness) || sharpness < 1 || sharpness > 16)
66
+ throw Error("Invalid triplanar sharpness");
67
+ if (offset.length !== 3 ||
68
+ offset.some((v) => !Number.isFinite(v) || Math.abs(v) > 1e6))
69
+ throw Error("Invalid triplanar offset");
70
+ if (parameters.bumpMap ||
71
+ parameters.displacementMap ||
72
+ (parameters.normalMapType !== undefined &&
73
+ parameters.normalMapType !== TangentSpaceNormalMap) ||
74
+ parameters.map?.isVideoTexture)
75
+ throw Error("Unsupported object-triplanar map type");
76
+ if (parameters.transparent ||
77
+ parameters.alphaHash ||
78
+ parameters.alphaToCoverage ||
79
+ (parameters.alphaTest ?? 0) > 0)
80
+ throw Error("Object triplanar mapping requires opaque coverage");
81
+ const material = new MeshStandardMaterial(parameters);
82
+ material.customProgramCacheKey = () => revision;
83
+ material.onBeforeRender = function (_renderer, _scene, _camera, _geometry, object) {
84
+ if (this.transparent ||
85
+ this.alphaHash ||
86
+ this.alphaToCoverage ||
87
+ this.alphaTest > 0)
88
+ throw Error("Object triplanar mapping requires opaque coverage");
89
+ if (this.bumpMap ||
90
+ this.displacementMap ||
91
+ this.normalMapType !== TangentSpaceNormalMap ||
92
+ this.map?.isVideoTexture)
93
+ throw Error("Unsupported object-triplanar map type");
94
+ const mesh = object;
95
+ if (mesh.isSkinnedMesh ||
96
+ mesh.morphTargetInfluences?.some((value) => value !== 0))
97
+ throw Error("Object triplanar mapping requires rigid geometry");
98
+ };
99
+ material.onBeforeCompile = (shader) => {
100
+ shader.uniforms.absTriScale = { value: scale };
101
+ shader.uniforms.absTriSharpness = { value: sharpness };
102
+ shader.uniforms.absTriOffset = {
103
+ value: new Vector3(offset[0], offset[1], offset[2]),
104
+ };
105
+ shader.vertexShader = replaceOnce(shader.vertexShader, "#include <common>", `#include <common>\n${varying}`);
106
+ shader.vertexShader = replaceOnce(shader.vertexShader, "#include <defaultnormal_vertex>", `#include <defaultnormal_vertex>
107
+ mat3 absTriFrame=mat3(1.);
108
+ #ifdef USE_BATCHING
109
+ absTriFrame=mat3(bm[0]/dot(bm[0],bm[0]),bm[1]/dot(bm[1],bm[1]),bm[2]/dot(bm[2],bm[2]))*absTriFrame;
110
+ #endif
111
+ #ifdef USE_INSTANCING
112
+ absTriFrame=mat3(im[0]/dot(im[0],im[0]),im[1]/dot(im[1],im[1]),im[2]/dot(im[2],im[2]))*absTriFrame;
113
+ #endif
114
+ absTriNormalToView=normalMatrix*absTriFrame;
115
+ absTriObjectNormal=objectNormal;
116
+ `);
117
+ shader.vertexShader = replaceOnce(shader.vertexShader, "#include <begin_vertex>", "#include <begin_vertex>\nabsTriPosition=transformed;");
118
+ // Normal-map declarations precede helper functions that use their uniforms.
119
+ shader.fragmentShader = replaceOnce(shader.fragmentShader, "#include <normalmap_pars_fragment>", `#include <normalmap_pars_fragment>\n${functions}`);
120
+ shader.fragmentShader = replaceOnce(shader.fragmentShader, "#include <map_fragment>", `${prepare}
121
+ #ifdef USE_MAP
122
+ diffuseColor*=absTriSample(map,absTriP,absTriDx,absTriDy,absTriW);
123
+ #endif
124
+ `);
125
+ shader.fragmentShader = replaceOnce(shader.fragmentShader, "#include <roughnessmap_fragment>", `
126
+ float roughnessFactor=roughness;
127
+ #ifdef USE_ROUGHNESSMAP
128
+ roughnessFactor*=absTriSample(roughnessMap,absTriP,absTriDx,absTriDy,absTriW).g;
129
+ #endif
130
+ `);
131
+ shader.fragmentShader = replaceOnce(shader.fragmentShader, "#include <normal_fragment_maps>", `
132
+ #ifdef USE_NORMALMAP_TANGENTSPACE
133
+ vec3 absTriG=absTriGradient(absTriP,absTriDx,absTriDy,absTriW);
134
+ vec3 absTriSurfaceG=absTriG-absTriN*dot(absTriG,absTriN);
135
+ if(dot(absTriSurfaceG,absTriSurfaceG)>1e-20) {
136
+ vec3 absTriBumped=normalize(absTriNormalToView*(absTriN-absTriSurfaceG));
137
+ #ifdef FLIP_SIDED
138
+ absTriBumped=-absTriBumped;
139
+ #endif
140
+ #ifdef DOUBLE_SIDED
141
+ absTriBumped*=faceDirection;
142
+ #endif
143
+ normal=absTriBumped;
144
+ }
145
+ #endif
146
+ `);
147
+ };
148
+ return material;
149
+ }
@@ -0,0 +1,10 @@
1
+ import { type ThreeElements } from "@react-three/fiber";
2
+ import type { WithAnimated } from "@react-spring/three";
3
+ /** Explicit Three hosts driven by React Spring's normal scheduler. Updating a
4
+ * Three prop invalidates its Fiber root; DOM-only springs do not render a scene.
5
+ * Does not install a global clock, call useFrame, or repair another module's
6
+ * scheduler changes. Avoid runtime imports of @react-spring/three alongside it. */
7
+ export declare function createFiberSpringHost<T extends keyof ThreeElements>(primitives: readonly T[]): Pick<WithAnimated, T>;
8
+ /** Small default host; opt into other Fiber primitives with createFiberSpringHost. */
9
+ export declare const animated: Pick<WithAnimated, "group" | "mesh">;
10
+ export { useSpring, easings } from "@react-spring/web";
package/dist/spring.js ADDED
@@ -0,0 +1,13 @@
1
+ import { applyProps } from "@react-three/fiber";
2
+ import { createHost } from "@react-spring/animated";
3
+ /** Explicit Three hosts driven by React Spring's normal scheduler. Updating a
4
+ * Three prop invalidates its Fiber root; DOM-only springs do not render a scene.
5
+ * Does not install a global clock, call useFrame, or repair another module's
6
+ * scheduler changes. Avoid runtime imports of @react-spring/three alongside it. */
7
+ export function createFiberSpringHost(primitives) {
8
+ return createHost([...primitives], { applyAnimatedValues: applyProps })
9
+ .animated;
10
+ }
11
+ /** Small default host; opt into other Fiber primitives with createFiberSpringHost. */
12
+ export const animated = createFiberSpringHost(["mesh", "group"]);
13
+ export { useSpring, easings } from "@react-spring/web";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@absolutejs/rendering",
3
- "version": "0.2.0",
4
- "description": "Render-destination coverage policies and callback-preserving material ownership for Three.js applications.",
3
+ "version": "0.4.0",
4
+ "description": "Three.js material ownership, destination coverage, rigid-object triplanar mapping and isolated React Spring hosts.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -10,6 +10,11 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js",
12
12
  "default": "./dist/index.js"
13
+ },
14
+ "./spring": {
15
+ "types": "./dist/spring.d.ts",
16
+ "import": "./dist/spring.js",
17
+ "default": "./dist/spring.js"
13
18
  }
14
19
  },
15
20
  "files": [
@@ -31,16 +36,49 @@
31
36
  "build": "tsc -p tsconfig.json",
32
37
  "test": "bun test ./tests",
33
38
  "check:package": "bun run build && bun run test",
34
- "test:browser": "bun scripts/verify-browser.cjs"
39
+ "test:browser": "bun scripts/verify-browser.cjs",
40
+ "test:spring:browser": "bun scripts/verify-spring.cjs",
41
+ "test:triplanar:browser": "bun scripts/verify-triplanar.cjs"
35
42
  },
36
43
  "peerDependencies": {
37
- "three": ">=0.186.0 <0.187.0"
44
+ "three": ">=0.186.0 <0.187.0",
45
+ "react": ">=19.3.0 <20",
46
+ "@react-three/fiber": ">=9.8.2 <10",
47
+ "@react-spring/animated": ">=10.1.2 <11",
48
+ "@react-spring/web": ">=10.1.2 <11",
49
+ "@react-spring/three": ">=10.1.2 <11"
38
50
  },
39
51
  "devDependencies": {
40
52
  "three": "0.186.1",
41
53
  "@types/three": "0.186.0",
42
54
  "@types/bun": "1.4.2",
43
55
  "typescript": "5.9.3",
44
- "playwright": "1.61.1"
56
+ "playwright": "1.61.1",
57
+ "react": "19.3.0",
58
+ "react-dom": "19.3.0",
59
+ "@types/react": "19.3.0",
60
+ "@types/react-dom": "19.3.0",
61
+ "@react-three/fiber": "9.8.2",
62
+ "@react-spring/animated": "10.1.2",
63
+ "@react-spring/web": "10.1.2",
64
+ "@react-spring/three": "10.1.2",
65
+ "@react-spring/shared": "10.1.2"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "react": {
69
+ "optional": true
70
+ },
71
+ "@react-three/fiber": {
72
+ "optional": true
73
+ },
74
+ "@react-spring/animated": {
75
+ "optional": true
76
+ },
77
+ "@react-spring/web": {
78
+ "optional": true
79
+ },
80
+ "@react-spring/three": {
81
+ "optional": true
82
+ }
45
83
  }
46
84
  }