pixospritz-specs 1.0.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.
@@ -0,0 +1,160 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://pixospritz.dev/schemas/matrix.spec.json",
4
+ "title": "Matrix Specification",
5
+ "description": "Shared matrix math specifications for JS and C engines",
6
+
7
+ "definitions": {
8
+ "Matrix4": {
9
+ "description": "4x4 transformation matrix in column-major order (WebGL/OpenGL compatible)",
10
+ "type": "object",
11
+ "properties": {
12
+ "data": {
13
+ "type": "array",
14
+ "items": { "type": "number" },
15
+ "minItems": 16,
16
+ "maxItems": 16,
17
+ "description": "16 floats in column-major order: [m00, m10, m20, m30, m01, m11, m21, m31, ...]"
18
+ }
19
+ },
20
+ "required": ["data"]
21
+ },
22
+
23
+ "Matrix3": {
24
+ "description": "3x3 matrix for rotations and normals",
25
+ "type": "object",
26
+ "properties": {
27
+ "data": {
28
+ "type": "array",
29
+ "items": { "type": "number" },
30
+ "minItems": 9,
31
+ "maxItems": 9
32
+ }
33
+ },
34
+ "required": ["data"]
35
+ }
36
+ },
37
+
38
+ "operations": {
39
+ "Matrix4": {
40
+ "identity": {
41
+ "signature": "() -> Matrix4",
42
+ "description": "Create identity matrix",
43
+ "result": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
44
+ "implementation": {
45
+ "js": "new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1])",
46
+ "c": "mat4_identity()"
47
+ }
48
+ },
49
+ "multiply": {
50
+ "signature": "(a: Matrix4, b: Matrix4) -> Matrix4",
51
+ "description": "Matrix multiplication (a * b)",
52
+ "implementation": {
53
+ "js": "mat4_multiply(a, b)",
54
+ "c": "mat4_multiply(a, b)"
55
+ }
56
+ },
57
+ "translate": {
58
+ "signature": "(m: Matrix4, v: Vector3) -> Matrix4",
59
+ "description": "Apply translation",
60
+ "implementation": {
61
+ "js": "mat4_translate(m, v.x, v.y, v.z)",
62
+ "c": "mat4_translate(m, v.x, v.y, v.z)"
63
+ }
64
+ },
65
+ "rotate": {
66
+ "signature": "(m: Matrix4, angle: number, axis: Vector3) -> Matrix4",
67
+ "description": "Apply rotation around axis (angle in radians)",
68
+ "implementation": {
69
+ "js": "mat4_rotate(m, angle, axis.x, axis.y, axis.z)",
70
+ "c": "mat4_rotate(m, angle, axis.x, axis.y, axis.z)"
71
+ }
72
+ },
73
+ "rotateX": {
74
+ "signature": "(m: Matrix4, angle: number) -> Matrix4",
75
+ "description": "Rotate around X axis",
76
+ "implementation": {
77
+ "js": "mat4_rotateX(m, angle)",
78
+ "c": "mat4_rotate_x(m, angle)"
79
+ }
80
+ },
81
+ "rotateY": {
82
+ "signature": "(m: Matrix4, angle: number) -> Matrix4",
83
+ "description": "Rotate around Y axis",
84
+ "implementation": {
85
+ "js": "mat4_rotateY(m, angle)",
86
+ "c": "mat4_rotate_y(m, angle)"
87
+ }
88
+ },
89
+ "rotateZ": {
90
+ "signature": "(m: Matrix4, angle: number) -> Matrix4",
91
+ "description": "Rotate around Z axis",
92
+ "implementation": {
93
+ "js": "mat4_rotateZ(m, angle)",
94
+ "c": "mat4_rotate_z(m, angle)"
95
+ }
96
+ },
97
+ "scale": {
98
+ "signature": "(m: Matrix4, v: Vector3) -> Matrix4",
99
+ "description": "Apply scaling",
100
+ "implementation": {
101
+ "js": "mat4_scale(m, v.x, v.y, v.z)",
102
+ "c": "mat4_scale(m, v.x, v.y, v.z)"
103
+ }
104
+ },
105
+ "perspective": {
106
+ "signature": "(fov: number, aspect: number, near: number, far: number) -> Matrix4",
107
+ "description": "Create perspective projection matrix (fov in radians)",
108
+ "implementation": {
109
+ "js": "mat4_perspective(fov, aspect, near, far)",
110
+ "c": "mat4_perspective(fov, aspect, near, far)"
111
+ },
112
+ "notes": "fov is vertical field of view in radians"
113
+ },
114
+ "orthographic": {
115
+ "signature": "(left: number, right: number, bottom: number, top: number, near: number, far: number) -> Matrix4",
116
+ "description": "Create orthographic projection matrix",
117
+ "implementation": {
118
+ "js": "mat4_ortho(left, right, bottom, top, near, far)",
119
+ "c": "mat4_ortho(left, right, bottom, top, near, far)"
120
+ }
121
+ },
122
+ "lookAt": {
123
+ "signature": "(eye: Vector3, target: Vector3, up: Vector3) -> Matrix4",
124
+ "description": "Create view matrix looking from eye to target",
125
+ "implementation": {
126
+ "js": "mat4_lookAt(eye, target, up)",
127
+ "c": "mat4_look_at(eye, target, up)"
128
+ }
129
+ },
130
+ "invert": {
131
+ "signature": "(m: Matrix4) -> Matrix4",
132
+ "description": "Compute inverse matrix",
133
+ "implementation": {
134
+ "js": "mat4_invert(m)",
135
+ "c": "mat4_invert(m)"
136
+ }
137
+ },
138
+ "transpose": {
139
+ "signature": "(m: Matrix4) -> Matrix4",
140
+ "description": "Transpose matrix",
141
+ "implementation": {
142
+ "js": "mat4_transpose(m)",
143
+ "c": "mat4_transpose(m)"
144
+ }
145
+ }
146
+ }
147
+ },
148
+
149
+ "layout": {
150
+ "description": "Column-major order for WebGL/OpenGL compatibility",
151
+ "indexMapping": {
152
+ "m00": 0, "m10": 1, "m20": 2, "m30": 3,
153
+ "m01": 4, "m11": 5, "m21": 6, "m31": 7,
154
+ "m02": 8, "m12": 9, "m22": 10, "m32": 11,
155
+ "m03": 12, "m13": 13, "m23": 14, "m33": 15
156
+ },
157
+ "translationIndices": [12, 13, 14],
158
+ "scaleIndices": [0, 5, 10]
159
+ }
160
+ }
@@ -0,0 +1,137 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://pixospritz.dev/schemas/vector.spec.json",
4
+ "title": "Vector Specification",
5
+ "description": "Shared vector math specifications for JS and C engines",
6
+
7
+ "definitions": {
8
+ "Vector2": {
9
+ "description": "2D vector/coordinate",
10
+ "type": "object",
11
+ "properties": {
12
+ "x": { "type": "number", "default": 0 },
13
+ "y": { "type": "number", "default": 0 }
14
+ },
15
+ "required": ["x", "y"]
16
+ },
17
+
18
+ "Vector3": {
19
+ "description": "3D vector",
20
+ "type": "object",
21
+ "properties": {
22
+ "x": { "type": "number", "default": 0 },
23
+ "y": { "type": "number", "default": 0 },
24
+ "z": { "type": "number", "default": 0 }
25
+ },
26
+ "required": ["x", "y", "z"]
27
+ },
28
+
29
+ "Vector4": {
30
+ "description": "4D vector (homogeneous coordinates)",
31
+ "type": "object",
32
+ "properties": {
33
+ "x": { "type": "number", "default": 0 },
34
+ "y": { "type": "number", "default": 0 },
35
+ "z": { "type": "number", "default": 0 },
36
+ "w": { "type": "number", "default": 1 }
37
+ },
38
+ "required": ["x", "y", "z", "w"]
39
+ }
40
+ },
41
+
42
+ "operations": {
43
+ "Vector3": {
44
+ "add": {
45
+ "signature": "(a: Vector3, b: Vector3) -> Vector3",
46
+ "description": "Component-wise addition",
47
+ "implementation": {
48
+ "js": "{ x: a.x + b.x, y: a.y + b.y, z: a.z + b.z }",
49
+ "c": "vec3_add(a, b)"
50
+ }
51
+ },
52
+ "sub": {
53
+ "signature": "(a: Vector3, b: Vector3) -> Vector3",
54
+ "description": "Component-wise subtraction",
55
+ "implementation": {
56
+ "js": "{ x: a.x - b.x, y: a.y - b.y, z: a.z - b.z }",
57
+ "c": "vec3_sub(a, b)"
58
+ }
59
+ },
60
+ "scale": {
61
+ "signature": "(v: Vector3, s: number) -> Vector3",
62
+ "description": "Scalar multiplication",
63
+ "implementation": {
64
+ "js": "{ x: v.x * s, y: v.y * s, z: v.z * s }",
65
+ "c": "vec3_scale(v, s)"
66
+ }
67
+ },
68
+ "dot": {
69
+ "signature": "(a: Vector3, b: Vector3) -> number",
70
+ "description": "Dot product",
71
+ "implementation": {
72
+ "js": "a.x * b.x + a.y * b.y + a.z * b.z",
73
+ "c": "vec3_dot(a, b)"
74
+ }
75
+ },
76
+ "cross": {
77
+ "signature": "(a: Vector3, b: Vector3) -> Vector3",
78
+ "description": "Cross product",
79
+ "implementation": {
80
+ "js": "{ x: a.y * b.z - a.z * b.y, y: a.z * b.x - a.x * b.z, z: a.x * b.y - a.y * b.x }",
81
+ "c": "vec3_cross(a, b)"
82
+ }
83
+ },
84
+ "length": {
85
+ "signature": "(v: Vector3) -> number",
86
+ "description": "Vector magnitude",
87
+ "implementation": {
88
+ "js": "Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)",
89
+ "c": "vec3_length(v)"
90
+ }
91
+ },
92
+ "normalize": {
93
+ "signature": "(v: Vector3) -> Vector3",
94
+ "description": "Unit vector",
95
+ "implementation": {
96
+ "js": "const len = length(v); return len > 0 ? scale(v, 1/len) : { x: 0, y: 0, z: 0 }",
97
+ "c": "vec3_normalize(v)"
98
+ }
99
+ },
100
+ "distance": {
101
+ "signature": "(a: Vector3, b: Vector3) -> number",
102
+ "description": "Euclidean distance between two points",
103
+ "implementation": {
104
+ "js": "length(sub(b, a))",
105
+ "c": "vec3_distance(a, b)"
106
+ }
107
+ },
108
+ "lerp": {
109
+ "signature": "(a: Vector3, b: Vector3, t: number) -> Vector3",
110
+ "description": "Linear interpolation",
111
+ "implementation": {
112
+ "js": "{ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t, z: a.z + (b.z - a.z) * t }",
113
+ "c": "vec3_lerp(a, b, t)"
114
+ }
115
+ },
116
+ "negate": {
117
+ "signature": "(v: Vector3) -> Vector3",
118
+ "description": "Negate all components",
119
+ "implementation": {
120
+ "js": "{ x: -v.x, y: -v.y, z: -v.z }",
121
+ "c": "vec3_negate(v)"
122
+ }
123
+ }
124
+ }
125
+ },
126
+
127
+ "constants": {
128
+ "ZERO": { "x": 0, "y": 0, "z": 0 },
129
+ "ONE": { "x": 1, "y": 1, "z": 1 },
130
+ "UP": { "x": 0, "y": 1, "z": 0 },
131
+ "DOWN": { "x": 0, "y": -1, "z": 0 },
132
+ "LEFT": { "x": -1, "y": 0, "z": 0 },
133
+ "RIGHT": { "x": 1, "y": 0, "z": 0 },
134
+ "FORWARD": { "x": 0, "y": 0, "z": -1 },
135
+ "BACK": { "x": 0, "y": 0, "z": 1 }
136
+ }
137
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "pixospritz-specs",
3
+ "version": "1.0.1",
4
+ "description": "Shared specifications, schemas, and constants for PixoSpritz engines",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./math/*": "./math/*.json",
10
+ "./formats/*": "./formats/*.json",
11
+ "./constants/*": "./constants/*.json",
12
+ "./shaders/*": "./shaders/*"
13
+ },
14
+ "bin": {
15
+ "generate-c-headers": "./generate-c-headers.cjs"
16
+ },
17
+ "scripts": {
18
+ "generate": "node generate-c-headers.cjs",
19
+ "generate:core-c": "node generate-c-headers.cjs --output-dir ../core-c/src/generated"
20
+ },
21
+ "files": [
22
+ "index.js",
23
+ "math/",
24
+ "formats/",
25
+ "constants/",
26
+ "shaders/",
27
+ "generate-c-headers.cjs"
28
+ ],
29
+ "keywords": [
30
+ "pixospritz",
31
+ "game-engine",
32
+ "specifications"
33
+ ],
34
+ "author": "ConflictingTheories",
35
+ "license": "MIT"
36
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Common Lighting Calculations
3
+ * Shared GLSL fragment for both JS (WebGL) and C (OpenGL) engines
4
+ *
5
+ * Include this in fragment shaders that need lighting:
6
+ * #include "lighting.glsl"
7
+ */
8
+
9
+ // Maximum number of lights supported
10
+ #define MAX_LIGHTS 8
11
+
12
+ // Light types
13
+ #define LIGHT_TYPE_POINT 0
14
+ #define LIGHT_TYPE_DIRECTIONAL 1
15
+ #define LIGHT_TYPE_SPOT 2
16
+
17
+ // Light structure (must match engine uniform layout)
18
+ struct Light {
19
+ vec3 position;
20
+ vec3 direction;
21
+ vec3 color;
22
+ float intensity;
23
+ float range;
24
+ float innerAngle;
25
+ float outerAngle;
26
+ int type;
27
+ bool enabled;
28
+ bool castShadow;
29
+ };
30
+
31
+ // Calculate attenuation for point/spot lights
32
+ float calculateAttenuation(float distance, float range) {
33
+ // Inverse square falloff with range limit
34
+ float attenuation = 1.0 / (1.0 + distance * distance);
35
+
36
+ // Smooth falloff at range boundary
37
+ float rangeAttenuation = 1.0 - smoothstep(range * 0.75, range, distance);
38
+
39
+ return attenuation * rangeAttenuation;
40
+ }
41
+
42
+ // Calculate spot light cone factor
43
+ float calculateSpotFactor(vec3 lightDir, vec3 spotDir, float innerAngle, float outerAngle) {
44
+ float theta = dot(lightDir, normalize(-spotDir));
45
+ float epsilon = innerAngle - outerAngle;
46
+ return clamp((theta - outerAngle) / epsilon, 0.0, 1.0);
47
+ }
48
+
49
+ // Diffuse lighting (Lambert)
50
+ float calculateDiffuse(vec3 normal, vec3 lightDir) {
51
+ return max(dot(normal, lightDir), 0.0);
52
+ }
53
+
54
+ // Specular lighting (Blinn-Phong)
55
+ float calculateSpecular(vec3 normal, vec3 lightDir, vec3 viewDir, float shininess) {
56
+ vec3 halfDir = normalize(lightDir + viewDir);
57
+ float specAngle = max(dot(normal, halfDir), 0.0);
58
+ return pow(specAngle, shininess);
59
+ }
60
+
61
+ // Calculate contribution from a single light
62
+ vec3 calculateLightContribution(
63
+ Light light,
64
+ vec3 fragPos,
65
+ vec3 normal,
66
+ vec3 viewDir,
67
+ vec3 albedo,
68
+ float shininess
69
+ ) {
70
+ if (!light.enabled) {
71
+ return vec3(0.0);
72
+ }
73
+
74
+ vec3 lightDir;
75
+ float attenuation = 1.0;
76
+
77
+ if (light.type == LIGHT_TYPE_DIRECTIONAL) {
78
+ // Directional light - no attenuation
79
+ lightDir = normalize(-light.direction);
80
+ } else {
81
+ // Point or spot light
82
+ vec3 toLight = light.position - fragPos;
83
+ float distance = length(toLight);
84
+ lightDir = normalize(toLight);
85
+ attenuation = calculateAttenuation(distance, light.range);
86
+
87
+ if (light.type == LIGHT_TYPE_SPOT) {
88
+ // Apply spot cone
89
+ float spotFactor = calculateSpotFactor(
90
+ lightDir,
91
+ light.direction,
92
+ light.innerAngle,
93
+ light.outerAngle
94
+ );
95
+ attenuation *= spotFactor;
96
+ }
97
+ }
98
+
99
+ // Diffuse
100
+ float diff = calculateDiffuse(normal, lightDir);
101
+ vec3 diffuse = diff * albedo * light.color;
102
+
103
+ // Specular
104
+ float spec = calculateSpecular(normal, lightDir, viewDir, shininess);
105
+ vec3 specular = spec * light.color;
106
+
107
+ return (diffuse + specular) * light.intensity * attenuation;
108
+ }
109
+
110
+ // Calculate total lighting from all lights
111
+ vec3 calculateTotalLighting(
112
+ Light lights[MAX_LIGHTS],
113
+ int numLights,
114
+ vec3 fragPos,
115
+ vec3 normal,
116
+ vec3 viewDir,
117
+ vec3 albedo,
118
+ vec3 ambient,
119
+ float shininess
120
+ ) {
121
+ // Start with ambient
122
+ vec3 result = ambient * albedo;
123
+
124
+ // Add contribution from each light
125
+ for (int i = 0; i < MAX_LIGHTS; i++) {
126
+ if (i >= numLights) break;
127
+ result += calculateLightContribution(
128
+ lights[i],
129
+ fragPos,
130
+ normal,
131
+ viewDir,
132
+ albedo,
133
+ shininess
134
+ );
135
+ }
136
+
137
+ return result;
138
+ }
139
+
140
+ // Simple ambient occlusion approximation
141
+ float calculateAO(vec3 normal, vec3 up) {
142
+ float ao = dot(normal, up) * 0.5 + 0.5;
143
+ return mix(0.5, 1.0, ao);
144
+ }
145
+
146
+ // Fresnel effect for rim lighting
147
+ float calculateFresnel(vec3 normal, vec3 viewDir, float power) {
148
+ float fresnel = 1.0 - max(dot(normal, viewDir), 0.0);
149
+ return pow(fresnel, power);
150
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Common Transform Calculations
3
+ * Shared GLSL functions for vertex transformations
4
+ *
5
+ * Include this in vertex shaders:
6
+ * #include "transforms.glsl"
7
+ */
8
+
9
+ // Billboard types
10
+ #define BILLBOARD_NONE 0
11
+ #define BILLBOARD_SPHERICAL 1 // Face camera fully
12
+ #define BILLBOARD_CYLINDRICAL 2 // Face camera horizontally only (Y-axis locked)
13
+ #define BILLBOARD_SCREEN 3 // Align to screen plane
14
+
15
+ /**
16
+ * Create a billboard rotation matrix that faces the camera
17
+ * @param viewMatrix The camera's view matrix
18
+ * @param billboardType Type of billboarding to apply
19
+ * @return 3x3 rotation matrix for billboard orientation
20
+ */
21
+ mat3 createBillboardMatrix(mat4 viewMatrix, int billboardType) {
22
+ if (billboardType == BILLBOARD_NONE) {
23
+ return mat3(1.0);
24
+ }
25
+
26
+ // Extract camera right and up vectors from view matrix
27
+ vec3 right = vec3(viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0]);
28
+ vec3 up = vec3(viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1]);
29
+ vec3 forward = vec3(viewMatrix[0][2], viewMatrix[1][2], viewMatrix[2][2]);
30
+
31
+ if (billboardType == BILLBOARD_CYLINDRICAL) {
32
+ // Lock Y-axis, only rotate around it
33
+ up = vec3(0.0, 1.0, 0.0);
34
+ right = normalize(cross(up, forward));
35
+ forward = cross(right, up);
36
+ } else if (billboardType == BILLBOARD_SCREEN) {
37
+ // Align exactly to screen plane
38
+ right = vec3(1.0, 0.0, 0.0);
39
+ up = vec3(0.0, 1.0, 0.0);
40
+ forward = vec3(0.0, 0.0, 1.0);
41
+ }
42
+
43
+ return mat3(right, up, forward);
44
+ }
45
+
46
+ /**
47
+ * Transform a vertex position for billboard rendering
48
+ * @param localPos Local vertex position (usually quad corners)
49
+ * @param worldPos World position of the billboard center
50
+ * @param scale Scale of the billboard
51
+ * @param viewMatrix Camera view matrix
52
+ * @param projMatrix Camera projection matrix
53
+ * @param billboardType Type of billboarding
54
+ * @return Clip space position
55
+ */
56
+ vec4 billboardTransform(
57
+ vec3 localPos,
58
+ vec3 worldPos,
59
+ vec2 scale,
60
+ mat4 viewMatrix,
61
+ mat4 projMatrix,
62
+ int billboardType
63
+ ) {
64
+ mat3 billboardRot = createBillboardMatrix(viewMatrix, billboardType);
65
+
66
+ // Apply scale and rotation to local position
67
+ vec3 scaledPos = vec3(localPos.x * scale.x, localPos.y * scale.y, localPos.z);
68
+ vec3 rotatedPos = billboardRot * scaledPos;
69
+
70
+ // Add to world position
71
+ vec4 worldVertex = vec4(worldPos + rotatedPos, 1.0);
72
+
73
+ // Transform to clip space
74
+ return projMatrix * viewMatrix * worldVertex;
75
+ }
76
+
77
+ /**
78
+ * Apply isometric projection offset
79
+ * Standard 2:1 isometric ratio
80
+ */
81
+ vec2 toIsometric(vec3 worldPos) {
82
+ float isoX = (worldPos.x - worldPos.z) * 0.5;
83
+ float isoY = (worldPos.x + worldPos.z) * 0.25 - worldPos.y * 0.5;
84
+ return vec2(isoX, isoY);
85
+ }
86
+
87
+ /**
88
+ * Convert screen coordinates to world ray
89
+ * For picking/raycasting
90
+ */
91
+ vec3 screenToWorldRay(vec2 screenPos, vec2 screenSize, mat4 invProjMatrix, mat4 invViewMatrix) {
92
+ // Normalize to [-1, 1]
93
+ vec2 ndc = (screenPos / screenSize) * 2.0 - 1.0;
94
+ ndc.y = -ndc.y; // Flip Y
95
+
96
+ // Create clip space position
97
+ vec4 clipPos = vec4(ndc, -1.0, 1.0);
98
+
99
+ // Transform to view space
100
+ vec4 viewPos = invProjMatrix * clipPos;
101
+ viewPos = vec4(viewPos.xy, -1.0, 0.0);
102
+
103
+ // Transform to world space
104
+ vec3 worldDir = (invViewMatrix * viewPos).xyz;
105
+
106
+ return normalize(worldDir);
107
+ }
108
+
109
+ /**
110
+ * Calculate sprite depth for proper sorting
111
+ * Considers Y position for top-down games
112
+ */
113
+ float calculateSpriteDepth(vec3 worldPos, float yBias) {
114
+ // Base depth from Z
115
+ float depth = worldPos.z;
116
+
117
+ // Add Y bias for proper sprite stacking
118
+ depth += worldPos.y * yBias;
119
+
120
+ return depth;
121
+ }
122
+
123
+ /**
124
+ * UV coordinate transformations for sprite sheets
125
+ */
126
+ vec2 transformUV(vec2 uv, vec2 frameOffset, vec2 frameSize, vec2 sheetSize) {
127
+ vec2 normalizedOffset = frameOffset / sheetSize;
128
+ vec2 normalizedSize = frameSize / sheetSize;
129
+ return normalizedOffset + uv * normalizedSize;
130
+ }
131
+
132
+ /**
133
+ * Flip UV coordinates
134
+ */
135
+ vec2 flipUV(vec2 uv, bool flipX, bool flipY) {
136
+ if (flipX) uv.x = 1.0 - uv.x;
137
+ if (flipY) uv.y = 1.0 - uv.y;
138
+ return uv;
139
+ }
140
+
141
+ /**
142
+ * Rotate UV coordinates
143
+ * @param uv UV coordinates
144
+ * @param angle Rotation in radians
145
+ * @param center Rotation center (usually 0.5, 0.5)
146
+ */
147
+ vec2 rotateUV(vec2 uv, float angle, vec2 center) {
148
+ float s = sin(angle);
149
+ float c = cos(angle);
150
+ vec2 offset = uv - center;
151
+ return vec2(
152
+ offset.x * c - offset.y * s,
153
+ offset.x * s + offset.y * c
154
+ ) + center;
155
+ }
156
+
157
+ /**
158
+ * Calculate parallax offset for depth effects
159
+ */
160
+ vec2 parallaxOffset(vec2 uv, vec3 viewDir, float height, float scale) {
161
+ float h = height * scale;
162
+ vec2 offset = viewDir.xy / viewDir.z * h;
163
+ return uv - offset;
164
+ }