model_viewer_pro 1.0.3 copy "model_viewer_pro: ^1.0.3" to clipboard
model_viewer_pro: ^1.0.3 copied to clipboard

Interactive 3D models in Flutter. Full runtime control over mesh visibility, colors, cameras, animations, and lighting. Built on model_viewer_plus.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:model_viewer_pro/model_viewer_pro.dart';

void main() {
  runApp(const MaterialApp(
    debugShowCheckedModeBanner: false,
    title: 'Flutter 3D — Demo',
    home: DemoScreen(),
  ));
}

/// Interactive demo for the [model_viewer_pro] package.
///
/// Demonstrates:
/// - Loading a GLB model from a Flutter asset
/// - Grounded mode with a local skybox image
/// - Runtime lighting controls (exposure, shadow intensity, shadow softness)
/// - Mesh discovery, visibility toggling, and colour changes
class DemoScreen extends StatefulWidget {
  const DemoScreen({super.key});

  @override
  State<DemoScreen> createState() => _DemoScreenState();
}

class _DemoScreenState extends State<DemoScreen> {
  final ModelViewerProController _controller = ModelViewerProController();

  List<String> _availableMeshes = [];
  final Map<String, bool> _visibilityMap = {};
  bool _isLoading = true;
  bool _isGrounded = true;
  double _exposure = 2.0;
  double _shadowIntensity = 1.0;
  double _shadowSoftness = 0.5;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[100],
      appBar: AppBar(
        title: const Text('Flutter 3D — Demo'),
        backgroundColor: Colors.deepPurple,
        foregroundColor: Colors.white,
        actions: [
          if (_isLoading)
            const Padding(
              padding: EdgeInsets.all(16),
              child: SizedBox(
                width: 20,
                height: 20,
                child: CircularProgressIndicator(
                  strokeWidth: 2,
                  color: Colors.white,
                ),
              ),
            ),
        ],
      ),
      body: Column(
        children: [
          // ── 3D Viewer ──────────────────────────────────────────────────────
          SizedBox(
            height: MediaQuery.of(context).size.height * 0.44,
            child: Stack(
              children: [
                Container(
                  color: Colors.grey[200],
                  child: ModelViewerProViewer(
                    src: 'assets/Astronaut.glb',
                    controller: _controller,
                    // Local asset skybox loaded via base-64 injection.
                    skyboxImagePath: 'assets/park.jpg',
                    // Grounded mode — projects skybox onto a ground plane.
                    grounded: _isGrounded,
                    exposure: _exposure,
                    shadowIntensity: _shadowIntensity,
                    shadowSoftness: _shadowSoftness,
                    autoRotate: false,
                    cameraControls: true,
                    backgroundColor: Colors.grey[200]!,
                    onLoad: (List<String> meshes) {
                      setState(() {
                        // Prepend a virtual 'WholeModel' target for bulk ops.
                        _availableMeshes = ['WholeModel', ...meshes];
                        _isLoading = false;
                        for (final name in _availableMeshes) {
                          _visibilityMap[name] = true;
                        }
                      });
                    },
                  ),
                ),
                if (_isLoading)
                  Container(
                    color: Colors.black38,
                    child: const Center(
                      child: Column(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          CircularProgressIndicator(color: Colors.white),
                          SizedBox(height: 16),
                          Text(
                            'Loading 3D model…',
                            style: TextStyle(
                              color: Colors.white,
                              fontSize: 15,
                              fontWeight: FontWeight.w600,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
              ],
            ),
          ),

          // ── Controls ────────────────────────────────────────────────────────
          Expanded(
            child: SingleChildScrollView(
              padding: const EdgeInsets.all(8),
              child: Column(
                children: [
                  _buildLightingCard(),
                  const SizedBox(height: 8),
                  _buildGroundedCard(),
                  const SizedBox(height: 8),
                  if (_availableMeshes.isNotEmpty)
                    _buildMeshCard()
                  else if (!_isLoading)
                    _buildRetryCard(),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  // ── Lighting card ──────────────────────────────────────────────────────────

  Widget _buildLightingCard() {
    return _Card(
      title: 'Lighting',
      color: Colors.deepPurple.shade50,
      borderColor: Colors.deepPurple.shade200,
      child: Column(
        children: [
          _Slider(
            label: 'Exposure',
            value: _exposure,
            min: 0.1,
            max: 3.0,
            onChanged: (v) => setState(() => _exposure = v),
          ),
          _Slider(
            label: 'Shadow intensity',
            value: _shadowIntensity,
            min: 0.0,
            max: 1.0,
            onChanged: (v) => setState(() => _shadowIntensity = v),
          ),
          _Slider(
            label: 'Shadow softness',
            value: _shadowSoftness,
            min: 0.0,
            max: 1.0,
            onChanged: (v) => setState(() => _shadowSoftness = v),
          ),
        ],
      ),
    );
  }

  // ── Grounded toggle card ───────────────────────────────────────────────────

  Widget _buildGroundedCard() {
    return _Card(
      title: 'Grounded mode',
      color: Colors.amber.shade50,
      borderColor: Colors.amber.shade200,
      child: SwitchListTile(
        title: Text(
          _isGrounded ? 'Enabled' : 'Disabled',
          style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
        ),
        subtitle: Text(
          _isGrounded
              ? 'Skybox projected on ground, pan locked, orbit ≤90°'
              : 'Free orbit, full skybox sphere',
          style: TextStyle(fontSize: 11, color: Colors.grey[600]),
        ),
        value: _isGrounded,
        activeThumbColor: Colors.amber[700],
        onChanged: (v) => setState(() => _isGrounded = v),
        contentPadding: EdgeInsets.zero,
      ),
    );
  }

  // ── Mesh list card ────────────────────────────────────────────────────────

  Widget _buildMeshCard() {
    return _Card(
      title: 'Meshes (${_availableMeshes.length})',
      color: Colors.cyan.shade50,
      borderColor: Colors.cyan.shade200,
      child: ConstrainedBox(
        constraints: const BoxConstraints(maxHeight: 320),
        child: ListView.builder(
          shrinkWrap: true,
          itemCount: _availableMeshes.length,
          itemBuilder: (context, index) {
            final name = _availableMeshes[index];
            final visible = _visibilityMap[name] ?? true;
            return ListTile(
              dense: true,
              contentPadding: EdgeInsets.zero,
              title: Text(name, style: const TextStyle(fontSize: 12)),
              trailing: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  // Visibility toggle
                  IconButton(
                    icon: Icon(
                      visible ? Icons.visibility : Icons.visibility_off,
                      color: visible ? Colors.deepPurple : Colors.grey,
                      size: 18,
                    ),
                    onPressed: () => _toggleVisibility(name, !visible),
                  ),
                  // Apply red colour
                  IconButton(
                    icon: const Icon(Icons.circle, color: Colors.red, size: 18),
                    tooltip: 'Red',
                    onPressed: () => _applyColor(name, '#FF0000'),
                  ),
                  // Apply green colour
                  IconButton(
                    icon:
                        const Icon(Icons.circle, color: Colors.green, size: 18),
                    tooltip: 'Green',
                    onPressed: () => _applyColor(name, '#00C853'),
                  ),
                ],
              ),
            );
          },
        ),
      ),
    );
  }

  // ── Retry card ────────────────────────────────────────────────────────────

  Widget _buildRetryCard() {
    return Center(
      child: ElevatedButton.icon(
        icon: const Icon(Icons.refresh),
        label: const Text('Retry loading meshes'),
        style: ElevatedButton.styleFrom(
            backgroundColor: Colors.deepPurple, foregroundColor: Colors.white),
        onPressed: () async {
          setState(() => _isLoading = true);
          await _controller.waitForSceneReady(timeoutMs: 5000);
          await Future.delayed(const Duration(milliseconds: 500));
          final meshes = await _controller.getAvailableMeshes();
          setState(() {
            _availableMeshes = ['WholeModel', ...meshes];
            for (final m in _availableMeshes) {
              _visibilityMap[m] = true;
            }
            _isLoading = false;
          });
        },
      ),
    );
  }

  // ── Helpers ───────────────────────────────────────────────────────────────

  void _toggleVisibility(String name, bool visible) {
    setState(() => _visibilityMap[name] = visible);
    if (name == 'WholeModel') {
      for (final m in _availableMeshes) {
        if (m != 'WholeModel') {
          setState(() => _visibilityMap[m] = visible);
          _controller.setVisibility(m, visible);
        }
      }
    } else {
      _controller.setVisibility(name, visible);
    }
  }

  void _applyColor(String name, String hex) {
    if (name == 'WholeModel') {
      for (final m in _availableMeshes) {
        if (m != 'WholeModel') _controller.setTextureColor(m, hex);
      }
    } else {
      _controller.setTextureColor(name, hex);
    }
  }
}

// ── Re-usable UI components ───────────────────────────────────────────────────

class _Card extends StatelessWidget {
  const _Card({
    required this.title,
    required this.child,
    required this.color,
    required this.borderColor,
  });

  final String title;
  final Widget child;
  final Color color;
  final Color borderColor;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: color,
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: borderColor),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(title,
              style:
                  const TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
          const SizedBox(height: 4),
          child,
        ],
      ),
    );
  }
}

class _Slider extends StatelessWidget {
  const _Slider({
    required this.label,
    required this.value,
    required this.min,
    required this.max,
    required this.onChanged,
  });

  final String label;
  final double value;
  final double min;
  final double max;
  final ValueChanged<double> onChanged;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        SizedBox(
          width: 110,
          child: Text(label, style: const TextStyle(fontSize: 12)),
        ),
        Expanded(
          child: Slider(
            value: value,
            min: min,
            max: max,
            activeColor: Colors.deepPurple,
            onChanged: onChanged,
          ),
        ),
        SizedBox(
          width: 34,
          child: Text(
            value.toStringAsFixed(2),
            style: const TextStyle(fontSize: 11, color: Colors.grey),
          ),
        ),
      ],
    );
  }
}
4
likes
160
points
178
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Interactive 3D models in Flutter. Full runtime control over mesh visibility, colors, cameras, animations, and lighting. Built on model_viewer_plus.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, model_viewer_plus, webview_flutter

More

Packages that depend on model_viewer_pro