@plasius/renderer 1.0.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/CODE_OF_CONDUCT.md +79 -0
  3. package/CONTRIBUTORS.md +27 -0
  4. package/LICENSE +203 -0
  5. package/README.md +70 -0
  6. package/SECURITY.md +17 -0
  7. package/dist/adaptivedpr.d.ts +2 -0
  8. package/dist/adaptivedpr.d.ts.map +1 -0
  9. package/dist/adaptivedpr.js +65 -0
  10. package/dist/camera/cameraRigProfile.d.ts +12 -0
  11. package/dist/camera/cameraRigProfile.d.ts.map +1 -0
  12. package/dist/camera/cameraRigProfile.js +18 -0
  13. package/dist/camera/managedCameraController.d.ts +49 -0
  14. package/dist/camera/managedCameraController.d.ts.map +1 -0
  15. package/dist/camera/managedCameraController.js +271 -0
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +3 -0
  19. package/dist/landscape.d.ts +2 -0
  20. package/dist/landscape.d.ts.map +1 -0
  21. package/dist/landscape.js +120 -0
  22. package/dist/player/player.d.ts +8 -0
  23. package/dist/player/player.d.ts.map +1 -0
  24. package/dist/player/player.js +203 -0
  25. package/dist/player/playerstore.d.ts +205 -0
  26. package/dist/player/playerstore.d.ts.map +1 -0
  27. package/dist/player/playerstore.js +500 -0
  28. package/dist/renderStateProvider.d.ts +57 -0
  29. package/dist/renderStateProvider.d.ts.map +1 -0
  30. package/dist/renderStateProvider.js +50 -0
  31. package/dist/renderer.d.ts +9 -0
  32. package/dist/renderer.d.ts.map +1 -0
  33. package/dist/renderer.js +165 -0
  34. package/dist/scene.d.ts +7 -0
  35. package/dist/scene.d.ts.map +1 -0
  36. package/dist/scene.js +10 -0
  37. package/dist/shaders/fragment/landscapeFragmentShader.js +141 -0
  38. package/dist/shaders/landscapeShader.d.ts +13 -0
  39. package/dist/shaders/landscapeShader.d.ts.map +1 -0
  40. package/dist/shaders/landscapeShader.js +25 -0
  41. package/dist/shaders/vertex/landscapeVertexShader.js +67 -0
  42. package/dist/styles/renderer.module.css +90 -0
  43. package/dist/worldSpaceCompositor.d.ts +50 -0
  44. package/dist/worldSpaceCompositor.d.ts.map +1 -0
  45. package/dist/worldSpaceCompositor.js +159 -0
  46. package/dist/xr/rendererXrBridge.d.ts +12 -0
  47. package/dist/xr/rendererXrBridge.d.ts.map +1 -0
  48. package/dist/xr/rendererXrBridge.js +17 -0
  49. package/docs/adrs/adr-0001-renderer-package-scope.md +21 -0
  50. package/docs/adrs/adr-0002-public-repo-governance.md +24 -0
  51. package/docs/adrs/adr-0003-world-space-compositor-contracts.md +34 -0
  52. package/docs/adrs/adr-template.md +35 -0
  53. package/docs/design/0001-public-package-scope.md +18 -0
  54. package/docs/tdrs/index.md +3 -0
  55. package/docs/tdrs/tdr-0001-renderer-public-package-standards-alignment.md +19 -0
  56. package/legal/CLA-REGISTRY.csv +1 -0
  57. package/legal/CLA.md +22 -0
  58. package/legal/CORPORATE_CLA.md +57 -0
  59. package/legal/INDIVIDUAL_CLA.md +91 -0
  60. package/package.json +117 -0
  61. package/src/adaptivedpr.tsx +74 -0
  62. package/src/camera/cameraRigProfile.ts +29 -0
  63. package/src/camera/managedCameraController.tsx +401 -0
  64. package/src/global.d.ts +10 -0
  65. package/src/index.ts +3 -0
  66. package/src/landscape.tsx +321 -0
  67. package/src/player/player.tsx +257 -0
  68. package/src/player/playerstore.tsx +733 -0
  69. package/src/renderStateProvider.tsx +121 -0
  70. package/src/renderer.tsx +294 -0
  71. package/src/scene.tsx +42 -0
  72. package/src/shaders/fragment/landscapeFragmentShader.d.ts +4 -0
  73. package/src/shaders/fragment/landscapeFragmentShader.js +141 -0
  74. package/src/shaders/landscapeShader.tsx +39 -0
  75. package/src/shaders/vertex/landscapeVertexShader.d.ts +4 -0
  76. package/src/shaders/vertex/landscapeVertexShader.js +67 -0
  77. package/src/styles/renderer.module.css +90 -0
  78. package/src/worldSpaceCompositor.ts +265 -0
  79. package/src/xr/rendererXrBridge.ts +44 -0
@@ -0,0 +1,121 @@
1
+ import { createScopedStoreContext } from "@plasius/react-state";
2
+ import type { IState } from "@plasius/react-state";
3
+ import type { CameraRigProfile } from "./camera/cameraRigProfile.js";
4
+ import { VIEW_PROFILE } from "./camera/cameraRigProfile.js";
5
+
6
+ type RenderAction =
7
+ | {
8
+ type: "set_camera_profile";
9
+ payload: CameraRigProfile;
10
+ }
11
+ | {
12
+ type: "set_frame";
13
+ payload: number;
14
+ }
15
+ | {
16
+ type: "set_last_render_time";
17
+ payload: number;
18
+ }
19
+ | {
20
+ type: "update";
21
+ }
22
+ | {
23
+ type: "reset";
24
+ }
25
+ | {
26
+ type: "set_frame_rate";
27
+ payload: number;
28
+ }
29
+ | {
30
+ type: "increment_accumulated_frames";
31
+ }
32
+ | {
33
+ type: "set_performance_tier";
34
+ payload: "low" | "medium" | "high" | "ultra";
35
+ }
36
+ | {
37
+ type: "set_is_animating";
38
+ payload: boolean;
39
+ }
40
+ | {
41
+ type: "set_pause_render";
42
+ payload: boolean;
43
+ }
44
+ | {
45
+ type: "set_scene_hash";
46
+ payload: string;
47
+ }
48
+ | {
49
+ type: "set_vr_mode";
50
+ payload: boolean;
51
+ };
52
+
53
+ interface RenderState extends IState {
54
+ frame: number;
55
+ lastRenderTime: number;
56
+ debugEnabled: boolean;
57
+ frameRate: number;
58
+ accumulatedFrames: number;
59
+ performanceTier: "low" | "medium" | "high" | "ultra";
60
+ isAnimating: boolean;
61
+ pauseRender: boolean;
62
+ sceneHash: string;
63
+ cameraRigProfile: CameraRigProfile;
64
+ useVR: boolean;
65
+ }
66
+
67
+ const initialState: RenderState = {
68
+ frame: 0,
69
+ lastRenderTime: 0,
70
+ debugEnabled: false,
71
+ frameRate: 0,
72
+ accumulatedFrames: 0,
73
+ performanceTier: "high",
74
+ isAnimating: true,
75
+ pauseRender: false,
76
+ sceneHash: "",
77
+ cameraRigProfile: VIEW_PROFILE,
78
+ useVR: false,
79
+ };
80
+
81
+ const reducer = (state: RenderState, action: RenderAction): RenderState => {
82
+ switch (action.type) {
83
+ case "update":
84
+ return { ...state };
85
+ case "reset":
86
+ return initialState;
87
+ case "set_frame_rate":
88
+ return { ...state, frameRate: action.payload };
89
+ case "increment_accumulated_frames":
90
+ return { ...state, accumulatedFrames: state.accumulatedFrames + 1 };
91
+ case "set_performance_tier":
92
+ return { ...state, performanceTier: action.payload };
93
+ case "set_is_animating":
94
+ return { ...state, isAnimating: action.payload };
95
+ case "set_pause_render":
96
+ return { ...state, pauseRender: action.payload };
97
+ case "set_scene_hash":
98
+ return { ...state, sceneHash: action.payload };
99
+ case "set_camera_profile":
100
+ return { ...state, cameraRigProfile: action.payload };
101
+ case "set_frame":
102
+ return { ...state, frame: action.payload };
103
+ case "set_last_render_time":
104
+ return { ...state, lastRenderTime: action.payload };
105
+ case "set_vr_mode":
106
+ return { ...state, useVR: action.payload };
107
+ default:
108
+ return state;
109
+ }
110
+ };
111
+
112
+ export type RenderStoreContext = ReturnType<
113
+ typeof createScopedStoreContext<RenderState, RenderAction>
114
+ >;
115
+
116
+ export const RenderStore: RenderStoreContext =
117
+ createScopedStoreContext<RenderState, RenderAction>(reducer, initialState);
118
+
119
+ export const RenderProvider = ({ children }: { children: React.ReactNode }) => {
120
+ return <RenderStore.Provider>{children}</RenderStore.Provider>;
121
+ };
@@ -0,0 +1,294 @@
1
+ import { type Vector3, type Group } from "three";
2
+ import * as THREE from "three/webgpu";
3
+ import {
4
+ Canvas,
5
+ //type ThreeToJSXElements,
6
+ type RootState,
7
+ } from "@react-three/fiber";
8
+ /*
9
+ declare module "@react-three/fiber" {
10
+ interface ThreeElements extends ThreeToJSXElements<typeof THREE> {
11
+
12
+ }
13
+ }
14
+ */
15
+ // Use the constructor parameter type as our renderer parameters type
16
+ type WebGPURendererParameters = ConstructorParameters<
17
+ typeof THREE.WebGPURenderer
18
+ >[0];
19
+ import { useRef, useEffect, Suspense } from "react";
20
+
21
+ import { Html, useProgress } from "@react-three/drei";
22
+ import { createXrManager } from "@plasius/gpu-xr";
23
+ import type { CameraManager } from "@plasius/gpu-camera";
24
+
25
+ import { BsHeadsetVr } from "react-icons/bs";
26
+ import { CgWebsite } from "react-icons/cg";
27
+
28
+ import { RenderStore } from "./renderStateProvider.js";
29
+
30
+ import styles from "./styles/renderer.module.css";
31
+ import { Scene } from "./scene.js";
32
+
33
+ import { Player } from "./player/player.js";
34
+
35
+ import { AdaptiveDPR } from "./adaptivedpr.js";
36
+ import {
37
+ bindSessionToRenderer,
38
+ rendererVrSessionInit,
39
+ } from "./xr/rendererXrBridge.js";
40
+ import {
41
+ createRendererCameraManager,
42
+ ManagedCameraController,
43
+ } from "./camera/managedCameraController.js";
44
+
45
+ interface WebGPURendererWithContext extends THREE.WebGPURenderer {
46
+ getContextAttributes: () => {
47
+ antialias: boolean;
48
+ alpha: boolean;
49
+ stencil: boolean;
50
+ depth: boolean;
51
+ powerPreference: string;
52
+ xrCompatible: boolean;
53
+ };
54
+ }
55
+
56
+ export interface RendererProps {
57
+ cameraPosition: Vector3;
58
+ cameraRotation: Vector3;
59
+ multiview: boolean;
60
+ }
61
+
62
+ function Loader() {
63
+ const { progress } = useProgress();
64
+ return <Html center>{progress} % loaded</Html>;
65
+ }
66
+
67
+ enum Perf {
68
+ Low,
69
+ Medium,
70
+ High,
71
+ Ultra,
72
+ }
73
+
74
+ function Renderer({
75
+ cameraPosition,
76
+ cameraRotation,
77
+ multiview = true,
78
+ children,
79
+ }: React.PropsWithChildren<RendererProps>) {
80
+ const canvasRef = useRef<HTMLCanvasElement>(null);
81
+ const sceneRef = useRef<Group | null>(null);
82
+ const rendererRef = useRef<WebGPURendererWithContext | null>(null);
83
+ const xrManagerRef = useRef<ReturnType<typeof createXrManager> | null>(null);
84
+ const cameraManagerRef = useRef<CameraManager | null>(null);
85
+
86
+ const { useVR, cameraRigProfile } = RenderStore.useStore();
87
+ const dispatch = RenderStore.useDispatch();
88
+
89
+ if (!cameraManagerRef.current) {
90
+ cameraManagerRef.current = createRendererCameraManager({
91
+ maxParallelViews: multiview ? 2 : 1,
92
+ maxHotCameras: 3,
93
+ });
94
+ }
95
+ const cameraManager = cameraManagerRef.current as CameraManager;
96
+
97
+ useEffect(() => {
98
+ return () => {
99
+ cameraManager.clear();
100
+ };
101
+ }, [cameraManager]);
102
+
103
+ useEffect(() => {
104
+ const xrManager = createXrManager({
105
+ onSessionStart: (session) => {
106
+ void bindSessionToRenderer(rendererRef.current, session).catch(() => {
107
+ dispatch({ type: "set_vr_mode", payload: false });
108
+ });
109
+ },
110
+ onSessionEnd: () => {
111
+ dispatch({ type: "set_vr_mode", payload: false });
112
+ },
113
+ });
114
+
115
+ xrManagerRef.current = xrManager;
116
+
117
+ void xrManager.probeSupport(["immersive-vr"]).catch(() => {
118
+ // Ignore probe failures; entering VR will surface actionable errors.
119
+ });
120
+
121
+ return () => {
122
+ const current = xrManagerRef.current;
123
+ xrManagerRef.current = null;
124
+ if (current) {
125
+ void current.dispose();
126
+ }
127
+ };
128
+ }, [dispatch]);
129
+
130
+ useEffect(() => {
131
+ const canvas = canvasRef.current;
132
+ const xrManager = xrManagerRef.current;
133
+
134
+ if (!canvas || !xrManager) {
135
+ return;
136
+ }
137
+
138
+ const fullscreenChangeHandler = () => {
139
+ if (!document.fullscreenElement) {
140
+ dispatch({ type: "set_vr_mode", payload: false });
141
+ }
142
+ };
143
+
144
+ const enterVRHandler = async () => {
145
+ try {
146
+ if (!document.fullscreenElement) {
147
+ await canvas.requestFullscreen();
148
+ }
149
+
150
+ canvas.addEventListener("fullscreenchange", fullscreenChangeHandler);
151
+ await xrManager.enterVr(rendererVrSessionInit);
152
+ } catch {
153
+ dispatch({ type: "set_vr_mode", payload: false });
154
+ }
155
+ };
156
+
157
+ const exitVRHandler = async () => {
158
+ await xrManager.exitSession();
159
+
160
+ if (document.fullscreenElement === canvas) {
161
+ await document.exitFullscreen().catch(() => {});
162
+ }
163
+ };
164
+
165
+ if (!useVR) {
166
+ void exitVRHandler();
167
+ return () => {
168
+ canvas.removeEventListener("fullscreenchange", fullscreenChangeHandler);
169
+ };
170
+ }
171
+
172
+ void enterVRHandler();
173
+
174
+ return () => {
175
+ canvas.removeEventListener("fullscreenchange", fullscreenChangeHandler);
176
+ };
177
+ }, [useVR, dispatch]);
178
+
179
+ return (
180
+ <>
181
+ <button
182
+ title={useVR ? "Exit VR" : "Enter VR"}
183
+ onClick={() => dispatch({ type: "set_vr_mode", payload: !useVR })}
184
+ className={styles.vrButton}
185
+ >
186
+ {useVR ? <CgWebsite size={24} /> : <BsHeadsetVr size={24} />}
187
+ </button>
188
+
189
+ <Suspense fallback={<div style={{ height: "100%" }} />}>
190
+ <Canvas
191
+ shadows={"soft"}
192
+ tabIndex={0}
193
+ ref={canvasRef}
194
+ className={`${styles.canvas} ${
195
+ !useVR ? styles.fixed : styles.absolute
196
+ }`}
197
+ style={{ width: "100%", height: "100%" }}
198
+ camera={{
199
+ position: cameraPosition.toArray(),
200
+ rotation: cameraRotation.toArray(),
201
+ near: 0.1,
202
+ far: 1000,
203
+ fov: 50,
204
+ }}
205
+ dpr={useVR ? 1 : [1, 4]}
206
+ gl={async (props) => {
207
+ const params = {
208
+ ...props,
209
+ multiview,
210
+ } as WebGPURendererParameters;
211
+ const renderer = new THREE.WebGPURenderer(
212
+ params
213
+ ) as WebGPURendererWithContext;
214
+
215
+ await renderer.init();
216
+
217
+ if (useVR && "setPixelRatio" in renderer) {
218
+ renderer.setPixelRatio(1);
219
+ }
220
+
221
+ if ("setClearColor" in renderer) {
222
+ renderer.setClearColor("lightblue");
223
+ }
224
+
225
+ if ("outputColorSpace" in renderer) {
226
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
227
+ }
228
+
229
+ if ("xr" in renderer) {
230
+ renderer.xr.enabled = true;
231
+ }
232
+
233
+ renderer.getContextAttributes = () => ({
234
+ antialias: props.antialias ?? true,
235
+ alpha: props.alpha ?? true,
236
+ stencil: props.stencil ?? true,
237
+ depth: props.depth ?? true,
238
+ powerPreference: props.powerPreference ?? "high-performance",
239
+ xrCompatible: true,
240
+ });
241
+ return renderer;
242
+ }}
243
+ onCreated={(state: RootState) => {
244
+ // In XR, keep DPR stable to avoid stereo stitching issues
245
+ if (useVR && "setPixelRatio" in state.gl) {
246
+ // Cap DPR to a reasonable value for HMDs; many runtimes manage internal resolution
247
+ state.gl.setPixelRatio(1);
248
+ }
249
+
250
+ if ("setClearColor" in state.gl) {
251
+ state.gl.setClearColor("lightblue");
252
+ }
253
+
254
+ if ("setSize" in state.gl) {
255
+ state.gl.setSize(state.size.width, state.size.height);
256
+ }
257
+
258
+ if ("outputColorSpace" in state.gl) {
259
+ state.gl.outputColorSpace = THREE.SRGBColorSpace;
260
+ }
261
+
262
+ rendererRef.current = state.gl as unknown as WebGPURendererWithContext;
263
+ }}
264
+ >
265
+ {!useVR && <AdaptiveDPR />}
266
+
267
+ <Suspense fallback={Loader()}>
268
+ {!useVR && (
269
+ <ManagedCameraController
270
+ manager={cameraManager}
271
+ profile={cameraRigProfile}
272
+ cameraId="main"
273
+ />
274
+ )}
275
+ <Scene ref={sceneRef}>
276
+ <Player cameraManager={cameraManager} cameraId="main" />
277
+ {children}
278
+ </Scene>
279
+ </Suspense>
280
+ </Canvas>
281
+ </Suspense>
282
+ </>
283
+ );
284
+ }
285
+
286
+ function WrappedRenderer(props: React.PropsWithChildren<RendererProps>) {
287
+ return (
288
+ <RenderStore.Provider>
289
+ <Renderer {...props} />
290
+ </RenderStore.Provider>
291
+ );
292
+ }
293
+
294
+ export { WrappedRenderer as Renderer };
package/src/scene.tsx ADDED
@@ -0,0 +1,42 @@
1
+ import { forwardRef, type ReactNode } from "react";
2
+ import { Physics } from "@react-three/rapier";
3
+ import { Environment } from "@react-three/drei";
4
+ import { Landscape } from "./landscape.js";
5
+ import { Vector3, Group } from "three";
6
+
7
+ export interface SceneProps {
8
+ children?: ReactNode[];
9
+ }
10
+
11
+ // Use forwardRef to make ref work
12
+ export const Scene = forwardRef<Group, SceneProps>(function Scene(props, ref) {
13
+ return (
14
+ <group ref={ref}>
15
+ <Environment
16
+ preset={"dawn"}
17
+ background={false}
18
+ environmentIntensity={0.5}
19
+ />
20
+ <ambientLight color={0xe1e1e1} intensity={0.3} />
21
+ <directionalLight
22
+ color={0xf0f0f0}
23
+ intensity={1.0}
24
+ position={new Vector3(5, 10, -10)}
25
+ castShadow
26
+ shadow-mapSize-width={2048}
27
+ shadow-mapSize-height={2048}
28
+ shadow-bias={-0.0005}
29
+ shadow-camera-left={-20}
30
+ shadow-camera-right={20}
31
+ shadow-camera-top={20}
32
+ shadow-camera-bottom={-20}
33
+ shadow-camera-near={0.5}
34
+ shadow-camera-far={100}
35
+ />
36
+ <Physics gravity={[0, -9.81, 0]}>
37
+ <Landscape />
38
+ {props.children}
39
+ </Physics>
40
+ </group>
41
+ );
42
+ });
@@ -0,0 +1,4 @@
1
+ import { ShaderNode } from "three/tsl";
2
+
3
+ // src/shaders/vertex/landscapeVertexShader.d.ts
4
+ export const landscapeFragmentShader: ShaderNode; // or object/function depending on your export
@@ -0,0 +1,141 @@
1
+ import * as TSL from "three/tsl";
2
+
3
+ // Fragment shader
4
+ export const landscapeFragmentShader = TSL.Fn((shader) => {
5
+ const vHeight = shader.input.float("vHeight");
6
+ const fragColor = shader.uniforms.vec4("fragColor");
7
+
8
+ const lowColor = TSL.vec3(0.5, 0.8, 0.3); // Grass
9
+ const highColor = TSL.vec3(1.0, 1.0, 1.0); // Snow
10
+
11
+ const mixFactor = TSL.smoothstep(0.0, 2.0, vHeight);
12
+ const baseColor = TSL.mix(lowColor, highColor, mixFactor);
13
+
14
+ TSL.assign(fragColor, TSL.vec4(baseColor, 1.0));
15
+ });
16
+
17
+ /*import * as TSL from "three/tsl";
18
+
19
+ // Fragment shader
20
+ export const landscapeFragmentShader = TSL.Fn((shader) => {
21
+ const vUv = shader.input.vec2("vUv");
22
+ const vHeight = shader.input.float("vHeight");
23
+ const vPos = shader.input.vec3("vPos");
24
+
25
+ const time = shader.uniforms.float("time");
26
+ const fragColor = shader.uniforms.vec4("fragColor");
27
+
28
+ const applyWind = (normal, pos) => {
29
+ const windDir = TSL.vec2(
30
+ TSL.sin(TSL.add(TSL.mul(pos.x, 0.1), TSL.mul(time, 0.3))),
31
+ TSL.cos(TSL.add(TSL.mul(pos.y, 0.1), TSL.mul(time, 0.3)))
32
+ );
33
+ const strength = TSL.add(
34
+ 0.2,
35
+ TSL.mul(
36
+ 0.1,
37
+ TSL.sin(TSL.add(TSL.dot(pos, TSL.vec2(0.3, 0.7)), TSL.mul(time, 0.5)))
38
+ )
39
+ );
40
+ const wind = TSL.mul(
41
+ TSL.normalize(TSL.vec3(windDir.x, windDir.y, 0.0)),
42
+ strength
43
+ );
44
+ return TSL.normalize(TSL.add(normal, TSL.mul(wind, 0.4)));
45
+ };
46
+
47
+ const calculateNormal = (height, uv, pos) => {
48
+ const eps = 0.01;
49
+ const hL = TSL.smoothstep(-2.0, 4.5, TSL.sub(height, eps));
50
+ const hR = TSL.smoothstep(-2.0, 4.5, TSL.add(height, eps));
51
+ const hD = TSL.smoothstep(-2.0, 4.5, TSL.sub(height, eps));
52
+ const hU = TSL.smoothstep(-2.0, 4.5, TSL.add(height, eps));
53
+ const normal = TSL.normalize(
54
+ TSL.vec3(TSL.sub(hL, hR), TSL.sub(hD, hU), 2.0)
55
+ );
56
+ return applyWind(normal, pos.xz);
57
+ };
58
+
59
+ const grassWave = (uv, height) => {
60
+ const baseFreq = 40.0;
61
+ const speed = 0.5;
62
+ const wave1 = TSL.sin(
63
+ TSL.mul(TSL.add(TSL.add(uv.x, uv.y), TSL.mul(time, speed)), baseFreq)
64
+ );
65
+ const wave2 = TSL.cos(
66
+ TSL.mul(
67
+ TSL.add(TSL.sub(uv.x, uv.y), TSL.mul(time, TSL.mul(speed, 1.2))),
68
+ TSL.mul(baseFreq, 0.8)
69
+ )
70
+ );
71
+ const pattern = TSL.mul(TSL.add(wave1, wave2), 0.03);
72
+ const density = TSL.smoothstep(0.3, 0.7, height);
73
+ return TSL.mul(pattern, density);
74
+ };
75
+
76
+ const grassDensityFactor = (coords) =>
77
+ TSL.smoothstep(
78
+ 0.4,
79
+ 0.9,
80
+ TSL.fract(
81
+ TSL.mul(
82
+ TSL.sin(TSL.dot(TSL.mul(coords, 0.2), TSL.vec2(12.9898, 78.233))),
83
+ 43758.5453
84
+ )
85
+ )
86
+ );
87
+
88
+ const sand = TSL.vec3(0.76, 0.7, 0.5);
89
+ const grass = TSL.vec3(0.4, 0.8, 0.2);
90
+ const stone = TSL.vec3(0.5, 0.5, 0.5);
91
+ const snow = TSL.vec3(0.95, 0.95, 0.95);
92
+
93
+ const low = TSL.smoothstep(-2.0, 0.0, vHeight);
94
+ const high = TSL.smoothstep(1.0, 3.0, vHeight);
95
+
96
+ let base = TSL.mix(sand, grass, low);
97
+ base = TSL.mix(base, stone, high);
98
+ base = TSL.mix(base, snow, TSL.smoothstep(2.5, 4.5, vHeight));
99
+
100
+ const normal = calculateNormal(vHeight, vUv, vPos);
101
+ const slope = TSL.dot(TSL.vec3(0.0, 0.0, 1.0), normal);
102
+ const snowBlend = TSL.smoothstep(0.8, 1.0, slope);
103
+ base = TSL.mix(
104
+ base,
105
+ snow,
106
+ TSL.mul(snowBlend, TSL.smoothstep(1.5, 4.5, vHeight))
107
+ );
108
+
109
+ const tintNoise = TSL.fract(
110
+ TSL.mul(TSL.sin(TSL.dot(vPos.xz, TSL.vec2(91.91, 47.47))), 43758.5453)
111
+ );
112
+ const tint = TSL.mix(
113
+ TSL.vec3(-0.05, 0.05, -0.02),
114
+ TSL.vec3(0.05, -0.03, 0.02),
115
+ tintNoise
116
+ );
117
+ base = TSL.add(base, tint);
118
+
119
+ const density = grassDensityFactor(vPos.xz);
120
+ base = TSL.mix(base, TSL.vec3(0.25, 0.4, 0.15), TSL.sub(1.0, density));
121
+
122
+ const lightDir = TSL.normalize(TSL.vec3(0.3, 0.5, 1.0));
123
+ let lighting = TSL.clamp(TSL.dot(lightDir, normal), 0.3, 1.0);
124
+ lighting = TSL.mul(lighting, TSL.add(0.8, TSL.mul(0.4, density)));
125
+
126
+ const grassMotion = TSL.mul(grassWave(vUv, vHeight), density);
127
+ const animatedGrass = TSL.vec3(0.0, TSL.mul(grassMotion, 0.4), 0.0);
128
+
129
+ const fresnel = TSL.pow(
130
+ TSL.sub(1.0, TSL.dot(TSL.normalize(TSL.vec3(0.0, 0.0, 1.0)), normal)),
131
+ 3.0
132
+ );
133
+ const fresnelColor = TSL.mul(TSL.vec3(0.2, 0.5, 0.1), fresnel);
134
+
135
+ const finalColor = TSL.mul(
136
+ TSL.add(TSL.add(base, animatedGrass), fresnelColor),
137
+ lighting
138
+ );
139
+ TSL.assign(fragColor, TSL.vec4(finalColor, 1.0));
140
+ });
141
+ */
@@ -0,0 +1,39 @@
1
+ import * as THREE from "three/webgpu";
2
+ import { extend, ThreeElements } from "@react-three/fiber";
3
+ import { landscapeVertexShader } from "./vertex/landscapeVertexShader.js";
4
+ import { landscapeFragmentShader } from "./fragment/landscapeFragmentShader.js";
5
+ import { add } from "three/tsl";
6
+
7
+ // ✅ Define a class that extends the material
8
+ class LandscapeShaderMaterial extends THREE.MeshStandardNodeMaterial {
9
+ constructor() {
10
+ super({ color: new THREE.Color(0.4, 0.7, 0.2) });
11
+ /*
12
+ // Append your vertex shader to the existing position pipeline
13
+ if (this.positionNode)
14
+ this.positionNode = shader(() =>
15
+ add(this.positionNode!, landscapeVertexShader)
16
+ );
17
+ else this.positionNode = landscapeVertexShader;
18
+ // Append your fragment shader to the existing color pipeline
19
+ if (this.colorNode)
20
+ this.colorNode = shader(() =>
21
+ add(this.colorNode!, landscapeFragmentShader)
22
+ );
23
+ else this.colorNode = landscapeFragmentShader;
24
+ */
25
+ }
26
+ }
27
+
28
+ // ✅ Register the class with R3F so you can use it as a JSX tag
29
+ extend({ LandscapeShaderMaterial });
30
+
31
+ export { LandscapeShaderMaterial };
32
+
33
+ declare module "@react-three/fiber" {
34
+ interface ThreeElements {
35
+ landscapeShaderMaterial: ThreeElements["meshStandardMaterial"] & {
36
+ ref?: React.Ref<THREE.MeshStandardNodeMaterial>;
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,4 @@
1
+ import { ShaderNode, Node, vec3 } from "three/tsl";
2
+
3
+ // src/shaders/vertex/landscapeVertexShader.d.ts
4
+ export const landscapeVertexShader: ShaderNode<Node, "vec3">;