Custom Rules Guide
This guide covers creating custom performance and quality rules in 3Lens to automatically detect issues and enforce standards in your Three.js applications.
Table of Contents
- Overview
- Quick Start
- Built-in Rules
- Creating Custom Rules
- Rule Conditions
- Rule Actions
- Rule Severity Levels
- Combining Rules
- Rule Configuration File
- Framework Integration
- Best Practices
Overview
3Lens rules allow you to:
- Automatically detect performance problems
- Enforce coding standards and best practices
- Highlight violations in the UI
- Block CI builds on rule failures
- Create custom domain-specific checks
Rules are evaluated every frame and can inspect:
- Frame statistics (FPS, draw calls, triangles)
- Scene structure (objects, materials, geometries)
- Memory usage (GPU memory, texture sizes)
- Custom metrics you define
Quick Start
Add performance rules in under 2 minutes:
typescript
import { createProbe } from '@3lens/core';
const probe = createProbe({
appName: 'My Game',
rules: {
// Simple threshold rules
maxDrawCalls: 500,
maxTriangles: 250000,
minFPS: 55,
maxFrameTimeMs: 18,
maxTextureMemoryMB: 256,
},
});
// Listen for violations
probe.onRuleViolation((violation) => {
console.warn(`Rule violation: ${violation.rule} - ${violation.message}`);
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
That's it! Violations appear in the 3Lens overlay and trigger callbacks.
Built-in Rules
Performance Rules
| Rule | Default | Description |
|---|---|---|
maxDrawCalls | - | Maximum draw calls per frame |
maxTriangles | - | Maximum triangles per frame |
minFPS | - | Minimum acceptable FPS |
maxFrameTimeMs | - | Maximum frame time in milliseconds |
maxGpuMemoryMB | - | Maximum GPU memory usage |
Resource Rules
| Rule | Default | Description |
|---|---|---|
maxTextures | - | Maximum number of textures |
maxTextureMemoryMB | - | Maximum texture memory |
maxGeometries | - | Maximum number of geometries |
maxMaterials | - | Maximum number of materials |
maxLights | - | Maximum number of lights |
Quality Rules
| Rule | Default | Description |
|---|---|---|
maxTextureSize | - | Maximum texture dimension (width/height) |
requireMipmaps | false | Require mipmaps on textures |
requirePowerOfTwo | false | Require power-of-two textures |
maxObjectDepth | - | Maximum scene graph depth |
Enabling Built-in Rules
typescript
const probe = createProbe({
appName: 'My App',
rules: {
// Performance
maxDrawCalls: 500,
maxTriangles: 250000,
minFPS: 55,
maxFrameTimeMs: 18,
// Resources
maxTextures: 100,
maxTextureMemoryMB: 256,
maxGeometries: 200,
maxMaterials: 50,
maxLights: 8,
// Quality
maxTextureSize: 2048,
requireMipmaps: true,
maxObjectDepth: 15,
},
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Creating Custom Rules
Rule Definition Interface
typescript
interface CustomRule {
/**
* Unique rule identifier
*/
id: string;
/**
* Human-readable name
*/
name: string;
/**
* Rule description
*/
description?: string;
/**
* Rule category for grouping
*/
category?: string;
/**
* Severity level
*/
severity: 'error' | 'warning' | 'info';
/**
* Evaluation function - return true if rule passes, false if violated
*/
evaluate: (context: RuleContext) => boolean | RuleResult;
/**
* Custom violation message (optional)
*/
message?: string | ((context: RuleContext) => string);
/**
* Rule is enabled
*/
enabled?: boolean;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Basic Custom Rule
typescript
import { createProbe, type CustomRule } from '@3lens/core';
const noTransparentMaterials: CustomRule = {
id: 'no-transparent-materials',
name: 'No Transparent Materials',
description: 'Disallow transparent materials for performance',
category: 'Performance',
severity: 'warning',
evaluate(context) {
const snapshot = context.getSnapshot();
const transparentCount = snapshot.materials.filter(m => m.transparent).length;
return transparentCount === 0;
},
message: (ctx) => {
const count = ctx.getSnapshot().materials.filter(m => m.transparent).length;
return `Found ${count} transparent material(s). Consider using alpha testing instead.`;
},
};
const probe = createProbe({
appName: 'My App',
customRules: [noTransparentMaterials],
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Rule with Details
typescript
const oversizedTextures: CustomRule = {
id: 'oversized-textures',
name: 'Oversized Textures',
description: 'Detect textures larger than recommended',
category: 'Memory',
severity: 'warning',
evaluate(context) {
const snapshot = context.getSnapshot();
const maxSize = context.getConfig('maxTextureSize') ?? 2048;
const oversized = snapshot.textures.filter(
t => t.width > maxSize || t.height > maxSize
);
if (oversized.length === 0) {
return true; // Pass
}
// Return detailed result
return {
passed: false,
message: `${oversized.length} texture(s) exceed ${maxSize}px`,
details: oversized.map(t => ({
name: t.name,
size: `${t.width}x${t.height}`,
recommended: `${maxSize}x${maxSize}`,
})),
affectedObjects: oversized.map(t => t.uuid),
};
},
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Stateful Rules
Track values over time:
typescript
const fpsStability: CustomRule = {
id: 'fps-stability',
name: 'FPS Stability',
description: 'Detect unstable frame rates',
category: 'Performance',
severity: 'warning',
evaluate(context) {
const stats = context.getFrameStats();
if (!stats) return true;
// Get history from rule state
const history = context.getRuleState<number[]>('fpsHistory') ?? [];
history.push(stats.fps);
// Keep last 60 frames
if (history.length > 60) history.shift();
context.setRuleState('fpsHistory', history);
// Need enough samples
if (history.length < 30) return true;
// Calculate standard deviation
const avg = history.reduce((a, b) => a + b, 0) / history.length;
const variance = history.reduce((sum, fps) => sum + Math.pow(fps - avg, 2), 0) / history.length;
const stdDev = Math.sqrt(variance);
// Fail if too much variance (>10% of average)
if (stdDev > avg * 0.1) {
return {
passed: false,
message: `Unstable FPS: avg ${avg.toFixed(1)}, stdDev ${stdDev.toFixed(1)}`,
};
}
return true;
},
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Rule Conditions
Frame Stats Conditions
typescript
evaluate(context) {
const stats = context.getFrameStats();
// Check FPS
if (stats.fps < 30) return false;
// Check frame time
if (stats.frameTimeMs > 33.33) return false;
// Check draw calls
if (stats.drawCalls > 1000) return false;
// Check triangles
if (stats.triangles > 500000) return false;
return true;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Scene Structure Conditions
typescript
evaluate(context) {
const snapshot = context.getSnapshot();
// Check object count
if (snapshot.nodes.length > 1000) return false;
// Check for specific object types
const meshes = snapshot.nodes.filter(n => n.type === 'Mesh');
const lights = snapshot.nodes.filter(n => n.type.includes('Light'));
// Check scene depth
const maxDepth = Math.max(...snapshot.nodes.map(n => n.depth));
return maxDepth <= 10;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Material Conditions
typescript
evaluate(context) {
const snapshot = context.getSnapshot();
// Check for expensive materials
const physicalMaterials = snapshot.materials.filter(
m => m.type === 'MeshPhysicalMaterial'
);
if (physicalMaterials.length > 10) {
return {
passed: false,
message: `Too many physical materials: ${physicalMaterials.length}`,
};
}
return true;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Memory Conditions
typescript
evaluate(context) {
const stats = context.getFrameStats();
const snapshot = context.getSnapshot();
// GPU memory
const gpuMemoryMB = (stats.memory?.gpuMemoryEstimate ?? 0) / (1024 * 1024);
if (gpuMemoryMB > 512) return false;
// Texture memory
const textureMemory = snapshot.textures.reduce(
(sum, t) => sum + (t.memorySize ?? 0), 0
);
const textureMemoryMB = textureMemory / (1024 * 1024);
if (textureMemoryMB > 256) return false;
return true;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Rule Actions
Highlighting Violations
typescript
const highlightRule: CustomRule = {
id: 'highlight-expensive',
name: 'Highlight Expensive Objects',
severity: 'info',
evaluate(context) {
const snapshot = context.getSnapshot();
const expensive = snapshot.nodes.filter(n =>
n.type === 'Mesh' &&
(n.geometry?.faceCount ?? 0) > 50000
);
if (expensive.length > 0) {
return {
passed: false,
affectedObjects: expensive.map(n => n.uuid),
highlightColor: '#ff6b6b', // Red highlight
};
}
return true;
},
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Automatic Fixes (Suggestions)
typescript
const textureFormatRule: CustomRule = {
id: 'texture-format',
name: 'Recommend Compressed Textures',
severity: 'info',
evaluate(context) {
const snapshot = context.getSnapshot();
const uncompressed = snapshot.textures.filter(t =>
!t.format?.includes('Compressed') && t.memorySize > 1024 * 1024
);
if (uncompressed.length > 0) {
return {
passed: false,
message: `${uncompressed.length} large uncompressed textures found`,
suggestions: uncompressed.map(t => ({
type: 'compress-texture',
target: t.uuid,
description: `Compress ${t.name} to save ${(t.memorySize / 1024 / 1024).toFixed(1)}MB`,
})),
};
}
return true;
},
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Rule Severity Levels
Error (Critical)
Blocks CI, shown prominently:
typescript
{
severity: 'error',
// Used for: Critical performance issues, crashes, memory leaks
}1
2
3
4
2
3
4
Warning
Highlighted in UI, may block CI:
typescript
{
severity: 'warning',
// Used for: Performance concerns, best practice violations
}1
2
3
4
2
3
4
Info
Informational, shown subtly:
typescript
{
severity: 'info',
// Used for: Suggestions, recommendations, tips
}1
2
3
4
2
3
4
Combining Rules
Rule Groups
typescript
import { createRuleGroup } from '@3lens/core';
const mobilePerformanceRules = createRuleGroup({
id: 'mobile-performance',
name: 'Mobile Performance',
description: 'Rules for mobile device optimization',
rules: [
{ id: 'mobile-draw-calls', ...drawCallsRule, threshold: 100 },
{ id: 'mobile-triangles', ...trianglesRule, threshold: 100000 },
{ id: 'mobile-texture-size', ...textureSizeRule, maxSize: 1024 },
],
});
const probe = createProbe({
appName: 'My App',
customRules: mobilePerformanceRules.rules,
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Conditional Rules
typescript
const conditionalRule: CustomRule = {
id: 'vr-performance',
name: 'VR Performance',
severity: 'error',
evaluate(context) {
// Only evaluate in VR mode
const isVR = context.getConfig('vrMode') ?? false;
if (!isVR) return true;
const stats = context.getFrameStats();
// VR requires 72+ FPS
if (stats.fps < 72) {
return {
passed: false,
message: `VR requires 72+ FPS, currently at ${stats.fps.toFixed(1)}`,
};
}
return true;
},
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Rule Configuration File
3lens.config.js
javascript
// 3lens.config.js
module.exports = {
appName: 'My Game',
rules: {
// Built-in rules with thresholds
maxDrawCalls: 500,
maxTriangles: 250000,
minFPS: 55,
maxFrameTimeMs: 18,
maxTextures: 100,
maxTextureMemoryMB: 256,
},
// Custom rules defined inline
customRules: [
{
id: 'no-emissive-materials',
name: 'No Emissive Materials',
severity: 'warning',
evaluate: (ctx) => {
const materials = ctx.getSnapshot().materials;
return !materials.some(m => m.emissive && m.emissiveIntensity > 0);
},
},
],
// Environment-specific overrides
environments: {
production: {
rules: {
maxDrawCalls: 300, // Stricter for production
minFPS: 60,
},
},
development: {
rules: {
minFPS: 30, // More lenient for development
},
},
},
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Loading Configuration
typescript
import { createProbe, loadConfig } from '@3lens/core';
// Auto-loads from 3lens.config.js
const config = await loadConfig();
const probe = createProbe(config);1
2
3
4
5
6
2
3
4
5
6
Framework Integration
React
tsx
import { useRuleViolations, useThreeLensProbe } from '@3lens/react-bridge';
function RuleViolationsPanel() {
const violations = useRuleViolations();
return (
<div className="violations-panel">
<h3>Rule Violations ({violations.length})</h3>
{violations.map(v => (
<div key={v.id} className={`violation ${v.severity}`}>
<strong>{v.rule}</strong>
<p>{v.message}</p>
</div>
))}
</div>
);
}
// Custom rule registration
function MyScene() {
const probe = useThreeLensProbe();
useEffect(() => {
probe.addCustomRule({
id: 'react-specific-rule',
name: 'React Specific',
severity: 'warning',
evaluate: (ctx) => {
// Custom logic
return true;
},
});
}, [probe]);
return <Canvas>...</Canvas>;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Vue
vue
<script setup>
import { useThreeLens, useRuleViolations } from '@3lens/vue-bridge';
import { onMounted } from 'vue';
const { probe } = useThreeLens();
const { violations } = useRuleViolations();
onMounted(() => {
probe.value?.addCustomRule({
id: 'vue-specific-rule',
name: 'Vue Specific',
severity: 'warning',
evaluate: (ctx) => true,
});
});
</script>
<template>
<div class="violations-panel">
<h3>Rule Violations ({{ violations.length }})</h3>
<div
v-for="v in violations"
:key="v.id"
:class="['violation', v.severity]"
>
<strong>{{ v.rule }}</strong>
<p>{{ v.message }}</p>
</div>
</div>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Best Practices
1. Keep Rules Fast
typescript
// ❌ Bad - Heavy computation every frame
evaluate(context) {
const distances = [];
for (const node of snapshot.nodes) {
for (const other of snapshot.nodes) {
distances.push(computeDistance(node, other)); // O(n²)
}
}
}
// ✅ Good - Sample or cache results
evaluate(context) {
const lastCheck = context.getRuleState('lastCheck') ?? 0;
if (Date.now() - lastCheck < 1000) return true; // Check once per second
context.setRuleState('lastCheck', Date.now());
// Now do expensive work
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2. Provide Actionable Messages
typescript
// ❌ Bad - Vague message
message: 'Performance issue detected'
// ✅ Good - Specific and actionable
message: (ctx) => {
const count = ctx.getSnapshot().textures.filter(t => t.width > 2048).length;
return `${count} textures exceed 2048px. Resize to 2048x2048 or use texture atlases.`;
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
3. Use Appropriate Severity
typescript
// error: Blocks CI, critical issues
// warning: Shows prominently, may block CI
// info: Suggestions only
severity: stats.fps < 15 ? 'error' : stats.fps < 30 ? 'warning' : 'info'1
2
3
4
5
2
3
4
5
4. Test Rules in Isolation
typescript
import { testRule } from '@3lens/core/testing';
describe('no-transparent-materials', () => {
it('passes when no transparent materials', () => {
const result = testRule(noTransparentMaterials, {
materials: [{ transparent: false }],
});
expect(result.passed).toBe(true);
});
it('fails when transparent materials exist', () => {
const result = testRule(noTransparentMaterials, {
materials: [{ transparent: true }],
});
expect(result.passed).toBe(false);
});
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17